houdini-react 2.0.0-next.41 → 2.0.0-next.45
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/package.json +8 -8
- package/postInstall.js +1 -1
- package/runtime/Link.tsx +24 -55
- package/runtime/hooks/useDocumentHandle.ts +9 -4
- package/runtime/hooks/useFragment.ts +169 -1
- package/runtime/hooks/useFragmentHandle.ts +84 -3
- package/runtime/hooks/useSubscription.ts +1 -1
- package/runtime/hooks/useSubscriptionHandle.ts +10 -6
- package/runtime/hydration.tsx +2 -2
- package/runtime/index.tsx +2 -2
- package/runtime/manifest.ts +1 -1
- package/runtime/mock.ts +9 -0
- package/runtime/resolve-href.ts +155 -5
- package/runtime/routes.ts +93 -0
- package/runtime/routing/Router.tsx +113 -45
- package/runtime/testing.tsx +163 -0
- package/vite/index.js +11 -3
- package/vite/strip-headers.d.ts +1 -0
- package/vite/strip-headers.js +35 -0
- package/vite/transform.d.ts +3 -1
- package/vite/transform.js +18 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "houdini-react",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.45",
|
|
4
4
|
"description": "The React plugin for houdini",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -81,13 +81,13 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"optionalDependencies": {
|
|
84
|
-
"houdini-react-darwin-x64": "2.0.0-next.
|
|
85
|
-
"houdini-react-darwin-arm64": "2.0.0-next.
|
|
86
|
-
"houdini-react-linux-x64": "2.0.0-next.
|
|
87
|
-
"houdini-react-linux-arm64": "2.0.0-next.
|
|
88
|
-
"houdini-react-win32-x64": "2.0.0-next.
|
|
89
|
-
"houdini-react-win32-arm64": "2.0.0-next.
|
|
90
|
-
"houdini-react-wasm": "2.0.0-next.
|
|
84
|
+
"houdini-react-darwin-x64": "2.0.0-next.45",
|
|
85
|
+
"houdini-react-darwin-arm64": "2.0.0-next.45",
|
|
86
|
+
"houdini-react-linux-x64": "2.0.0-next.45",
|
|
87
|
+
"houdini-react-linux-arm64": "2.0.0-next.45",
|
|
88
|
+
"houdini-react-win32-x64": "2.0.0-next.45",
|
|
89
|
+
"houdini-react-win32-arm64": "2.0.0-next.45",
|
|
90
|
+
"houdini-react-wasm": "2.0.0-next.45"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"compile": "scripts build-go",
|
package/postInstall.js
CHANGED
|
@@ -5,7 +5,7 @@ const https = require('https')
|
|
|
5
5
|
const child_process = require('child_process')
|
|
6
6
|
|
|
7
7
|
// Adjust the version you want to install. You can also make this dynamic.
|
|
8
|
-
const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.
|
|
8
|
+
const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.45'
|
|
9
9
|
|
|
10
10
|
// Windows binaries end with .exe so we need to special case them.
|
|
11
11
|
const binaryName = process.platform === 'win32' ? 'houdini-react.exe' : 'houdini-react'
|
package/runtime/Link.tsx
CHANGED
|
@@ -1,81 +1,50 @@
|
|
|
1
1
|
// this file is generated by houdini — do not edit
|
|
2
2
|
// @refresh reset
|
|
3
3
|
import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
|
|
4
|
+
import { getCurrentConfig } from '$houdini/runtime/config'
|
|
4
5
|
import React from 'react'
|
|
5
6
|
|
|
6
7
|
// @ts-ignore
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
import type { RouteScalars } from './manifest.js'
|
|
10
|
-
|
|
11
|
-
import { resolveHref } from './resolve-href.js'
|
|
12
|
-
|
|
13
|
-
type _Pages = (typeof rawManifest)['pages']
|
|
14
|
-
type _TSType<T extends string> = T extends keyof RouteScalars
|
|
15
|
-
? RouteScalars[T]
|
|
16
|
-
: T extends 'Int' | 'Float'
|
|
17
|
-
? number
|
|
18
|
-
: T extends 'ID'
|
|
19
|
-
? string | number
|
|
20
|
-
: T extends 'Boolean'
|
|
21
|
-
? boolean
|
|
22
|
-
: string
|
|
23
|
-
type _Param = { readonly name: string; readonly type: string; readonly optional: boolean }
|
|
24
|
-
type _ParamObj<Ps extends readonly _Param[]> = {
|
|
25
|
-
[P in Ps[number] as P['optional'] extends true ? P['name'] : never]?: _TSType<P['type']>
|
|
26
|
-
} & {
|
|
27
|
-
[P in Ps[number] as P['optional'] extends true ? never : P['name']]: _TSType<P['type']>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type _ExternalHref =
|
|
31
|
-
| `http://${string}`
|
|
32
|
-
| `https://${string}`
|
|
33
|
-
| `mailto:${string}`
|
|
34
|
-
| `tel:${string}`
|
|
35
|
-
| `blob:${string}`
|
|
36
|
-
| `data:${string}`
|
|
37
|
-
| `//${string}`
|
|
38
|
-
| `#${string}`
|
|
39
|
-
| `./${string}`
|
|
40
|
-
| `../${string}`
|
|
8
|
+
import manifest from './manifest.js'
|
|
9
|
+
import type { RouteHrefs, ExternalHref, ParamsForRoute, SearchForRoute } from './routes.js'
|
|
41
10
|
|
|
42
|
-
|
|
43
|
-
export type RouteHrefs = _Pages[keyof _Pages] extends { readonly url: infer U extends string }
|
|
44
|
-
? U
|
|
45
|
-
: never
|
|
11
|
+
import { buildHref, type RouteHrefInfo } from './resolve-href.js'
|
|
46
12
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// - 'params' is a separate intersection that errors when required but absent
|
|
50
|
-
type _PageForRoute<H extends string> = Extract<_Pages[keyof _Pages], { readonly url: H }>
|
|
51
|
-
type _ParamsForRoute<H extends string> = [_PageForRoute<H>] extends [never]
|
|
52
|
-
? { params?: never }
|
|
53
|
-
: _PageForRoute<H> extends { readonly params: readonly [] }
|
|
54
|
-
? { params?: never }
|
|
55
|
-
: _PageForRoute<H> extends { readonly params: infer Ps extends readonly _Param[] }
|
|
56
|
-
? { params: _ParamObj<Ps> }
|
|
57
|
-
: { params?: never }
|
|
13
|
+
// re-exported for custom link wrappers that constrain their own `to` prop
|
|
14
|
+
export type { RouteHrefs }
|
|
58
15
|
|
|
59
|
-
export type LinkProps<H extends RouteHrefs |
|
|
16
|
+
export type LinkProps<H extends RouteHrefs | ExternalHref = RouteHrefs | ExternalHref> = Omit<
|
|
60
17
|
DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
|
|
61
18
|
'href'
|
|
62
19
|
> & {
|
|
63
20
|
to: H
|
|
64
21
|
disabled?: boolean
|
|
65
22
|
preload?: boolean | 'data' | 'component' | 'page'
|
|
66
|
-
} &
|
|
23
|
+
} & ParamsForRoute<H> &
|
|
24
|
+
SearchForRoute<H>
|
|
67
25
|
|
|
68
|
-
export function Link<H extends RouteHrefs |
|
|
26
|
+
export function Link<H extends RouteHrefs | ExternalHref>({
|
|
69
27
|
to,
|
|
70
28
|
params,
|
|
29
|
+
search,
|
|
71
30
|
disabled,
|
|
72
31
|
preload,
|
|
73
32
|
...rest
|
|
74
33
|
}: LinkProps<H>): React.ReactElement {
|
|
34
|
+
// look up the destination route (O(1) via the codegen'd pagesByUrl map) so custom-scalar
|
|
35
|
+
// param/search values can be marshaled into their transport form (e.g. a Date →
|
|
36
|
+
// timestamp) before they hit the URL. External hrefs won't match a page, so marshalers
|
|
37
|
+
// stay empty.
|
|
38
|
+
const m = manifest as any
|
|
39
|
+
const page = m.pages[m.pagesByUrl[to as string]] as RouteHrefInfo | undefined
|
|
75
40
|
const href = disabled
|
|
76
41
|
? undefined
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
42
|
+
: buildHref(
|
|
43
|
+
to as string,
|
|
44
|
+
page,
|
|
45
|
+
getCurrentConfig()?.scalars,
|
|
46
|
+
params as Record<string, unknown> | undefined,
|
|
47
|
+
search as Record<string, unknown> | undefined
|
|
48
|
+
)
|
|
80
49
|
return React.createElement('a', { ...rest, href, 'data-houdini-preload': preload })
|
|
81
50
|
}
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
import type { DocumentStore } from 'houdini/runtime/client'
|
|
17
17
|
import React from 'react'
|
|
18
18
|
|
|
19
|
-
import { useClient,
|
|
19
|
+
import { useClient, useLocationContext, useSession } from '../routing/Router.js'
|
|
20
20
|
|
|
21
21
|
export function useDocumentHandle<
|
|
22
22
|
_Artifact extends QueryArtifact,
|
|
@@ -36,7 +36,7 @@ export function useDocumentHandle<
|
|
|
36
36
|
// Stable cursor stacks for SinglePage pagination — must survive re-renders caused by store updates
|
|
37
37
|
const previousCursorsRef = React.useRef<(string | null)[]>([])
|
|
38
38
|
const nextCursorsRef = React.useRef<(string | null)[]>([])
|
|
39
|
-
const location =
|
|
39
|
+
const location = useLocationContext()
|
|
40
40
|
|
|
41
41
|
// grab the current session value
|
|
42
42
|
const [session] = useSession()
|
|
@@ -234,5 +234,10 @@ type RefetchHandlers<_Artifact extends QueryArtifact, _Data extends GraphQLObjec
|
|
|
234
234
|
loadNext: OffsetHandlers<_Data, _Input>['loadNextPage']
|
|
235
235
|
loadNextPending: boolean
|
|
236
236
|
}
|
|
237
|
-
: //
|
|
238
|
-
{}
|
|
237
|
+
: // a @refetchable fragment: embedded query keyed by id, re-run with new args
|
|
238
|
+
_Artifact extends { refetch: { paginated: false } }
|
|
239
|
+
? {
|
|
240
|
+
refetch: (variables?: Partial<_Input>) => Promise<_Data>
|
|
241
|
+
}
|
|
242
|
+
: // the artifact does not support a known pagination method, don't add anything
|
|
243
|
+
{}
|
|
@@ -4,12 +4,36 @@ import type {
|
|
|
4
4
|
GraphQLVariables,
|
|
5
5
|
FragmentArtifact,
|
|
6
6
|
QueryResult,
|
|
7
|
+
SubscriptionSpec,
|
|
7
8
|
} from 'houdini/runtime'
|
|
8
9
|
import * as React from 'react'
|
|
9
10
|
|
|
10
11
|
import { useRouterContext } from '../routing/index.js'
|
|
11
12
|
import { useDocumentSubscription } from './useDocumentSubscription.js'
|
|
12
13
|
|
|
14
|
+
// useFragment reads a fragment's data back out of the cache. When the fragment is marked
|
|
15
|
+
// @plural it is spread on a list field, so the reference is an array of fragment references
|
|
16
|
+
// and the hook returns an array of data (see usePluralFragment below).
|
|
17
|
+
|
|
18
|
+
// plural overloads: the reference is an array of fragment references
|
|
19
|
+
export function useFragment<
|
|
20
|
+
_Data extends GraphQLObject,
|
|
21
|
+
_ReferenceType extends {},
|
|
22
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
23
|
+
>(
|
|
24
|
+
reference: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }>,
|
|
25
|
+
document: { artifact: FragmentArtifact }
|
|
26
|
+
): _Data[]
|
|
27
|
+
export function useFragment<
|
|
28
|
+
_Data extends GraphQLObject,
|
|
29
|
+
_ReferenceType extends {},
|
|
30
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
31
|
+
>(
|
|
32
|
+
reference: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }> | null,
|
|
33
|
+
document: { artifact: FragmentArtifact }
|
|
34
|
+
): _Data[] | null
|
|
35
|
+
|
|
36
|
+
// singular overload: the reference is a single fragment reference
|
|
13
37
|
export function useFragment<
|
|
14
38
|
_Data extends GraphQLObject,
|
|
15
39
|
_ReferenceType extends {},
|
|
@@ -17,6 +41,35 @@ export function useFragment<
|
|
|
17
41
|
>(
|
|
18
42
|
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
19
43
|
document: { artifact: FragmentArtifact }
|
|
44
|
+
): _Data | null
|
|
45
|
+
|
|
46
|
+
export function useFragment(reference: any, document: { artifact: FragmentArtifact }): any {
|
|
47
|
+
const plural = Boolean(document.artifact.plural)
|
|
48
|
+
|
|
49
|
+
// a non-plural fragment given a list of references is a mistake (the fragment needs
|
|
50
|
+
// @plural to be read as an array)
|
|
51
|
+
if (!plural && Array.isArray(reference)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`useFragment received a list of references for "${document.artifact.name}", but it is not marked @plural.`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Both implementations run on every render so the rules of hooks are preserved; the one
|
|
58
|
+
// that isn't relevant is fed a null reference and becomes a no-op. Which result we return
|
|
59
|
+
// is keyed off the static @plural artifact flag, so a given call-site is always consistent.
|
|
60
|
+
const singularResult = useSingularFragment(plural ? null : reference, document)
|
|
61
|
+
const pluralResult = usePluralFragment(plural ? (reference ?? null) : null, document)
|
|
62
|
+
|
|
63
|
+
return plural ? pluralResult : singularResult
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function useSingularFragment<
|
|
67
|
+
_Data extends GraphQLObject,
|
|
68
|
+
_ReferenceType extends {},
|
|
69
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
70
|
+
>(
|
|
71
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
72
|
+
document: { artifact: FragmentArtifact }
|
|
20
73
|
): _Data | null {
|
|
21
74
|
const { cache } = useRouterContext()
|
|
22
75
|
const { parent, variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
|
|
@@ -67,7 +120,9 @@ export function useFragment<
|
|
|
67
120
|
artifact: document.artifact,
|
|
68
121
|
variables,
|
|
69
122
|
initialValue: cachedValue,
|
|
70
|
-
|
|
123
|
+
// no parent means there is nothing to subscribe to (eg a null reference, or this
|
|
124
|
+
// singular hook running in no-op mode for a @plural fragment)
|
|
125
|
+
disabled: loading || !parent,
|
|
71
126
|
send: {
|
|
72
127
|
stuff: {
|
|
73
128
|
parentID: parent,
|
|
@@ -80,6 +135,119 @@ export function useFragment<
|
|
|
80
135
|
return storeValue.data
|
|
81
136
|
}
|
|
82
137
|
|
|
138
|
+
// usePluralFragment consumes a @plural fragment: the reference is an array of fragment
|
|
139
|
+
// references (one per item in the list the fragment was spread on). Each item is bound to
|
|
140
|
+
// its own cache record, so we keep the list in state and register one cache subscription
|
|
141
|
+
// per item inside a single effect. A subscription message carries the new value for just
|
|
142
|
+
// that record, so we patch it into the list in place rather than re-reading everything.
|
|
143
|
+
// Doing the subscriptions in one effect keeps the hook count stable regardless of how many
|
|
144
|
+
// items the list contains (we can't call a hook per item).
|
|
145
|
+
function usePluralFragment<
|
|
146
|
+
_Data extends GraphQLObject,
|
|
147
|
+
_ReferenceType extends {},
|
|
148
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
149
|
+
>(
|
|
150
|
+
references: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }> | null,
|
|
151
|
+
document: { artifact: FragmentArtifact }
|
|
152
|
+
): _Data[] | null {
|
|
153
|
+
const { cache } = useRouterContext()
|
|
154
|
+
const artifact = document.artifact
|
|
155
|
+
|
|
156
|
+
// resolve the cache record + variables for each reference in the list
|
|
157
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: document is a stable import
|
|
158
|
+
const entries = React.useMemo(
|
|
159
|
+
() =>
|
|
160
|
+
references
|
|
161
|
+
? references.map((reference) =>
|
|
162
|
+
fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
|
|
163
|
+
)
|
|
164
|
+
: null,
|
|
165
|
+
[references]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// a stable key describing which records (and variables) we are bound to, so we only
|
|
169
|
+
// re-seed and re-subscribe when the set of records actually changes (eg an insert/remove)
|
|
170
|
+
const subscriptionKey = entries
|
|
171
|
+
? entries
|
|
172
|
+
.map((entry) => `${entry.parent}:${JSON.stringify(entry.variables ?? {})}`)
|
|
173
|
+
.join('|')
|
|
174
|
+
: ''
|
|
175
|
+
|
|
176
|
+
// read every item from the cache. used to seed the list, and to re-seed it when the set
|
|
177
|
+
// of records changes (membership changes don't necessarily message the existing records).
|
|
178
|
+
const readAll = (): _Data[] | null => {
|
|
179
|
+
if (!references || !entries) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
return entries.map(({ parent, variables, loading }, i) =>
|
|
183
|
+
parent
|
|
184
|
+
? (cache.read({ selection: artifact.selection, parent, variables, loading })
|
|
185
|
+
.data as _Data)
|
|
186
|
+
: (references[i] as _Data)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const [data, setData] = React.useState<_Data[] | null>(readAll)
|
|
191
|
+
|
|
192
|
+
// the subscriptionKey the current `data` was seeded for; starts matching the initial
|
|
193
|
+
// useState seed so we don't redundantly re-seed on mount
|
|
194
|
+
const seededKey = React.useRef(subscriptionKey)
|
|
195
|
+
|
|
196
|
+
// re-seed when the set of records changes, then subscribe to each record individually.
|
|
197
|
+
// onMessage carries the new value for just that record, so we patch it in place.
|
|
198
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on subscriptionKey
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
if (seededKey.current !== subscriptionKey) {
|
|
201
|
+
seededKey.current = subscriptionKey
|
|
202
|
+
setData(readAll())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!entries) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const specs: SubscriptionSpec[] = []
|
|
210
|
+
entries.forEach(({ parent, variables, loading }, index) => {
|
|
211
|
+
if (!parent || loading) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const spec: SubscriptionSpec = {
|
|
215
|
+
rootType: artifact.rootType,
|
|
216
|
+
kind: artifact.kind,
|
|
217
|
+
selection: artifact.selection,
|
|
218
|
+
parentID: parent,
|
|
219
|
+
variables: () => variables ?? {},
|
|
220
|
+
onMessage: (message) => {
|
|
221
|
+
// only a cache write ('update') carries a new value for this record. other
|
|
222
|
+
// messages (eg 'refetch' from cache.refresh()) don't apply to a fragment,
|
|
223
|
+
// which has no refetch of its own; the refreshed value arrives as an 'update'.
|
|
224
|
+
if (message.kind !== 'update') {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
setData((current) => {
|
|
228
|
+
if (!current) {
|
|
229
|
+
return current
|
|
230
|
+
}
|
|
231
|
+
const next = current.slice()
|
|
232
|
+
next[index] = message.data as _Data
|
|
233
|
+
return next
|
|
234
|
+
})
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
cache.subscribe(spec)
|
|
238
|
+
specs.push(spec)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return () => {
|
|
242
|
+
for (const spec of specs) {
|
|
243
|
+
cache.unsubscribe(spec)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}, [subscriptionKey])
|
|
247
|
+
|
|
248
|
+
return data
|
|
249
|
+
}
|
|
250
|
+
|
|
83
251
|
export function fragmentReference<_Data extends GraphQLObject, _Input, _ReferenceType extends {}>(
|
|
84
252
|
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
85
253
|
document: { artifact: FragmentArtifact }
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
extractPageInfo,
|
|
3
|
+
cursorHandlers,
|
|
4
|
+
offsetHandlers,
|
|
5
|
+
getCurrentConfig,
|
|
6
|
+
entityRefetchVariables,
|
|
7
|
+
} from 'houdini/runtime'
|
|
2
8
|
import type {
|
|
3
9
|
GraphQLObject,
|
|
4
10
|
FragmentArtifact,
|
|
@@ -71,8 +77,44 @@ export function useFragmentHandle<
|
|
|
71
77
|
return (paginationData as any)[rootField] ?? null
|
|
72
78
|
}, [paginationData, refetchArtifact])
|
|
73
79
|
|
|
80
|
+
// @refetchable fragments embed the fragment in a query keyed by id (paginated: false).
|
|
81
|
+
// We observe that query so refetch() can swap in fresh data computed with new argument
|
|
82
|
+
// values, just like SinglePage pagination swaps in the latest page.
|
|
83
|
+
const isRefetchable = !!refetchArtifact?.refetch && !refetchArtifact.refetch.paginated
|
|
84
|
+
|
|
85
|
+
const refetchObserver = React.useMemo(() => {
|
|
86
|
+
if (!isRefetchable || !refetchArtifact) return null
|
|
87
|
+
return client.observe<_Data, _Input>({ artifact: refetchArtifact })
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
+
}, [refetchArtifact?.name, isRefetchable])
|
|
90
|
+
|
|
91
|
+
const subscribeToRefetch = React.useCallback(
|
|
92
|
+
(fn: () => void) => refetchObserver?.subscribe(() => fn()) ?? (() => {}),
|
|
93
|
+
[refetchObserver]
|
|
94
|
+
)
|
|
95
|
+
const getRefetchSnapshot = React.useCallback(
|
|
96
|
+
() => refetchObserver?.state.data ?? null,
|
|
97
|
+
[refetchObserver]
|
|
98
|
+
)
|
|
99
|
+
const refetchData = React.useSyncExternalStore(
|
|
100
|
+
subscribeToRefetch,
|
|
101
|
+
getRefetchSnapshot,
|
|
102
|
+
getRefetchSnapshot
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const refetchEntityData = React.useMemo<_Data | null>(() => {
|
|
106
|
+
if (!refetchData || !refetchArtifact?.selection?.fields) return null
|
|
107
|
+
const rootField = Object.keys(refetchArtifact.selection.fields)[0]
|
|
108
|
+
if (!rootField) return null
|
|
109
|
+
return (refetchData as any)[rootField] ?? null
|
|
110
|
+
}, [refetchData, refetchArtifact])
|
|
111
|
+
|
|
74
112
|
const displayData =
|
|
75
|
-
isSinglePage && paginationEntityData !== null
|
|
113
|
+
isSinglePage && paginationEntityData !== null
|
|
114
|
+
? paginationEntityData
|
|
115
|
+
: isRefetchable && refetchEntityData !== null
|
|
116
|
+
? refetchEntityData
|
|
117
|
+
: fragmentData
|
|
76
118
|
|
|
77
119
|
const wrapLoad = <_Result>(
|
|
78
120
|
setLoading: (val: boolean) => void,
|
|
@@ -170,9 +212,48 @@ export function useFragmentHandle<
|
|
|
170
212
|
return null
|
|
171
213
|
}, [refetchArtifact, paginationObserver, displayData, session, forwardPending, backwardPending])
|
|
172
214
|
|
|
215
|
+
// the fragment's current argument values: the initial args overlaid with everything that
|
|
216
|
+
// has been passed to refetch() so far. we track these explicitly rather than reading them
|
|
217
|
+
// back off the embedded query, whose variables also carry the synthetic id-lookup keys.
|
|
218
|
+
const [refetchArgs, setRefetchArgs] = React.useState<Partial<_Input>>({})
|
|
219
|
+
|
|
220
|
+
// re-run the embedded query with new argument values. the entity's id is derived from
|
|
221
|
+
// the fragment data (the parent reference), which always carries the visible id. we must
|
|
222
|
+
// NOT derive it from displayData: after a refetch that becomes the embedded query result,
|
|
223
|
+
// which masks the entity's id out of its selection, so reading it back would yield
|
|
224
|
+
// `id: undefined` and clobber the real id on a second refetch.
|
|
225
|
+
const refetch = React.useMemo(() => {
|
|
226
|
+
if (!isRefetchable || !refetchObserver || !refetchArtifact) return undefined
|
|
227
|
+
return (newVariables?: _Input) => {
|
|
228
|
+
setRefetchArgs((prev) => ({ ...prev, ...newVariables }))
|
|
229
|
+
const idVariables = entityRefetchVariables(
|
|
230
|
+
getCurrentConfig(),
|
|
231
|
+
refetchArtifact.refetch?.targetType,
|
|
232
|
+
fragmentData as Record<string, any> | null
|
|
233
|
+
)
|
|
234
|
+
return refetchObserver.send({
|
|
235
|
+
variables: {
|
|
236
|
+
...(refetchObserver.state.variables ?? variables),
|
|
237
|
+
...idVariables,
|
|
238
|
+
...newVariables,
|
|
239
|
+
} as _Input,
|
|
240
|
+
// suppress loading-state placeholder data during the transition so the
|
|
241
|
+
// currently displayed value stays put until the fresh result arrives
|
|
242
|
+
stuff: { silenceLoading: true },
|
|
243
|
+
cacheParams: { disableSubscriptions: true, disablePartial: true },
|
|
244
|
+
session,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}, [isRefetchable, refetchObserver, refetchArtifact, fragmentData, variables, session])
|
|
248
|
+
|
|
249
|
+
const displayVariables = isRefetchable
|
|
250
|
+
? ({ ...(variables as Record<string, any>), ...refetchArgs } as _Input)
|
|
251
|
+
: variables
|
|
252
|
+
|
|
173
253
|
return {
|
|
174
254
|
...handle,
|
|
175
|
-
variables,
|
|
255
|
+
variables: displayVariables,
|
|
176
256
|
data: displayData,
|
|
257
|
+
refetch,
|
|
177
258
|
}
|
|
178
259
|
}
|
|
@@ -5,7 +5,7 @@ import { useSubscriptionHandle } from './useSubscriptionHandle.js'
|
|
|
5
5
|
// a hook to subscribe to a subscription artifact
|
|
6
6
|
export function useSubscription<_Result extends GraphQLObject, _Input extends GraphQLVariables>(
|
|
7
7
|
document: { artifact: SubscriptionArtifact },
|
|
8
|
-
variables
|
|
8
|
+
variables?: _Input
|
|
9
9
|
) {
|
|
10
10
|
const { data } = useSubscriptionHandle(document, variables)
|
|
11
11
|
return data
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SubscriptionArtifact,
|
|
3
|
+
DocumentArtifact,
|
|
3
4
|
GraphQLObject,
|
|
4
5
|
GraphQLVariables,
|
|
5
6
|
GraphQLError,
|
|
@@ -18,20 +19,23 @@ export type SubscriptionHandle<_Result extends GraphQLObject, _Input extends Gra
|
|
|
18
19
|
|
|
19
20
|
// a hook to subscribe to a subscription artifact
|
|
20
21
|
export function useSubscriptionHandle<
|
|
21
|
-
_Result extends GraphQLObject,
|
|
22
|
-
_Input extends GraphQLVariables,
|
|
23
|
-
>(
|
|
22
|
+
_Result extends GraphQLObject = GraphQLObject,
|
|
23
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
24
|
+
>(
|
|
25
|
+
{ artifact }: { artifact: SubscriptionArtifact },
|
|
26
|
+
variables?: _Input
|
|
27
|
+
): SubscriptionHandle<_Result, _Input> {
|
|
24
28
|
// a subscription is basically just a live document
|
|
25
|
-
const [storeValue, observer] = useDocumentSubscription({
|
|
29
|
+
const [storeValue, observer] = useDocumentSubscription<DocumentArtifact, _Result, _Input>({
|
|
26
30
|
artifact,
|
|
27
|
-
variables,
|
|
31
|
+
variables: variables as _Input,
|
|
28
32
|
})
|
|
29
33
|
|
|
30
34
|
return {
|
|
31
35
|
data: storeValue.data,
|
|
32
36
|
errors: storeValue.errors,
|
|
33
37
|
fetching: storeValue.fetching,
|
|
34
|
-
variables,
|
|
38
|
+
variables: variables as _Input,
|
|
35
39
|
unlisten: observer.cleanup,
|
|
36
40
|
listen: observer.send,
|
|
37
41
|
}
|
package/runtime/hydration.tsx
CHANGED
|
@@ -112,7 +112,7 @@ export function hydrate_page(
|
|
|
112
112
|
hydrateRoot(
|
|
113
113
|
document,
|
|
114
114
|
<App
|
|
115
|
-
initialURL={window.location.pathname}
|
|
115
|
+
initialURL={window.location.pathname + window.location.search}
|
|
116
116
|
cache={window.__houdini__cache__}
|
|
117
117
|
session={window.__houdini__initial__session__}
|
|
118
118
|
{...window.__houdini__nav_caches__}
|
|
@@ -145,7 +145,7 @@ export function mount_static_app(App: React.ComponentType<any>, manifest: any) {
|
|
|
145
145
|
|
|
146
146
|
root.render(
|
|
147
147
|
React.createElement(App, {
|
|
148
|
-
initialURL: window.location.pathname,
|
|
148
|
+
initialURL: window.location.pathname + window.location.search,
|
|
149
149
|
cache: cacheRef,
|
|
150
150
|
session: null,
|
|
151
151
|
manifest,
|
package/runtime/index.tsx
CHANGED
|
@@ -10,9 +10,7 @@ export {
|
|
|
10
10
|
router_cache,
|
|
11
11
|
useCache,
|
|
12
12
|
useSession,
|
|
13
|
-
useLocation,
|
|
14
13
|
useRoute,
|
|
15
|
-
useCurrentVariables,
|
|
16
14
|
notFound,
|
|
17
15
|
unauthorized,
|
|
18
16
|
forbidden,
|
|
@@ -23,7 +21,9 @@ export {
|
|
|
23
21
|
RoutingError,
|
|
24
22
|
RedirectError,
|
|
25
23
|
} from './routing/index.js'
|
|
24
|
+
export type { GenericRoute } from './routing/index.js'
|
|
26
25
|
export * from './Link.js'
|
|
26
|
+
export { createMock } from './mock.js'
|
|
27
27
|
|
|
28
28
|
export function Router({
|
|
29
29
|
cache,
|
package/runtime/manifest.ts
CHANGED
package/runtime/mock.ts
ADDED