houdini-react 2.0.0-next.40 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini-react",
3
- "version": "2.0.0-next.40",
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.40",
85
- "houdini-react-darwin-arm64": "2.0.0-next.40",
86
- "houdini-react-linux-x64": "2.0.0-next.40",
87
- "houdini-react-linux-arm64": "2.0.0-next.40",
88
- "houdini-react-win32-x64": "2.0.0-next.40",
89
- "houdini-react-win32-arm64": "2.0.0-next.40",
90
- "houdini-react-wasm": "2.0.0-next.40"
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.40'
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 type rawManifest from './manifest.js'
8
- // @ts-ignore
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
- // All known app route URL strings — useful as a constraint for custom link wrappers.
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
- // Separate 'to' from 'params' so TypeScript evaluates them independently:
48
- // - 'to' completions show all routes (including parameterized) because 'to' itself is always valid
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 | _ExternalHref = RouteHrefs | _ExternalHref> = Omit<
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
- } & _ParamsForRoute<H>
23
+ } & ParamsForRoute<H> &
24
+ SearchForRoute<H>
67
25
 
68
- export function Link<H extends RouteHrefs | _ExternalHref>({
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
- : params != null
78
- ? resolveHref(to as string, params as Record<string, string | number | boolean>)
79
- : (to as string)
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, useLocation, useSession } from '../routing/Router.js'
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 = useLocation()
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
- : // the artifact does not support a known pagination method, don't add anything
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
- disabled: loading,
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 { extractPageInfo, cursorHandlers, offsetHandlers } from 'houdini/runtime'
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,
@@ -24,10 +30,7 @@ export function useFragmentHandle<
24
30
  reference: _Data | { ' $fragments': _ReferenceType } | null,
25
31
  document: { artifact: FragmentArtifact; refetchArtifact?: QueryArtifact }
26
32
  ): any {
27
- // get the fragment values
28
33
  const fragmentData = useFragment<_Data, _ReferenceType, _Input>(reference, document)
29
-
30
- // look at the fragment reference to get the variables
31
34
  const { variables } = fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
32
35
 
33
36
  const client = useClient()
@@ -36,30 +39,25 @@ export function useFragmentHandle<
36
39
  const [forwardPending, setForwardPending] = React.useState(false)
37
40
  const [backwardPending, setBackwardPending] = React.useState(false)
38
41
 
39
- // Stable cursor stacks for SinglePage pagination — must survive re-renders
40
42
  const previousCursorsRef = React.useRef<(string | null)[]>([])
41
43
  const nextCursorsRef = React.useRef<(string | null)[]>([])
42
44
 
43
45
  const refetchArtifact = document.refetchArtifact as QueryArtifact | undefined
44
46
  const refetchPath = refetchArtifact?.refetch?.path
45
47
 
46
- // Dedicated observer for pagination queries — separate from the fragment observer.
47
- // cursorHandlers derives entity variables (e.g. { id }) and artifact defaults
48
- // automatically via the type config, so no manual variable extraction is needed here.
49
48
  const paginationObserver = React.useMemo(() => {
50
49
  if (!refetchArtifact?.refetch?.paginated) return null
51
50
  return client.observe<_Data, _Input>({ artifact: refetchArtifact })
52
51
  }, [refetchArtifact?.name])
53
52
 
54
- // Subscribe to the pagination observer so React re-renders whenever a new page is fetched
55
- // or served from cache (CacheOrNetwork). The fragment store subscription only watches the
56
- // initial page's cache key; the observer is the live source of truth for SinglePage
57
- // pagination where each page lives at its own per-cursor cache key.
53
+ const isSinglePage = refetchArtifact?.refetch?.mode === 'SinglePage'
54
+
55
+ // Subscribe to the pagination observer so the component re-renders when a new page lands.
56
+ // For SinglePage we pass disablePartial so partial cache hits (entity found but connection
57
+ // not yet fetched) are never resolved back to the observer — we go straight to the network
58
+ // and update the observer only once we have a complete page.
58
59
  const subscribeToObserver = React.useCallback(
59
- (onChange: () => void) => {
60
- if (!paginationObserver) return () => {}
61
- return paginationObserver.subscribe(onChange)
62
- },
60
+ (fn: () => void) => paginationObserver?.subscribe(() => fn()) ?? (() => {}),
63
61
  [paginationObserver]
64
62
  )
65
63
  const getObserverSnapshot = React.useCallback(
@@ -72,30 +70,51 @@ export function useFragmentHandle<
72
70
  getObserverSnapshot
73
71
  )
74
72
 
75
- // Extract entity-level data from the pagination query response. For Node targetType
76
- // the response is { node: EntityData }; we take the first root field to handle any type.
77
- // Guard against partial cache hits (artifact has partial:true): only use the entity once the
78
- // paginated connection field at refetch.path[0] is actually present in the response.
79
73
  const paginationEntityData = React.useMemo<_Data | null>(() => {
80
74
  if (!paginationData || !refetchArtifact?.selection?.fields) return null
81
75
  const rootField = Object.keys(refetchArtifact.selection.fields)[0]
82
76
  if (!rootField) return null
83
- const entity = (paginationData as any)[rootField]
84
- if (!entity) return null
85
- const path = refetchArtifact.refetch?.path
86
- if (path && path.length > 0 && (entity as any)[path[0]] == null) {
87
- return null
88
- }
89
- return entity as _Data
77
+ return (paginationData as any)[rootField] ?? null
90
78
  }, [paginationData, refetchArtifact])
91
79
 
92
- const isSinglePage = refetchArtifact?.refetch?.mode === 'SinglePage'
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])
93
111
 
94
- // For SinglePage: use the pagination observer's entity data (each page has its own
95
- // cache key) once a page fetch has landed. For Infinite: always use fragmentData,
96
- // which reads accumulated pages from cache via the fragment's cache subscription.
97
112
  const displayData =
98
- isSinglePage && paginationEntityData !== null ? paginationEntityData : fragmentData
113
+ isSinglePage && paginationEntityData !== null
114
+ ? paginationEntityData
115
+ : isRefetchable && refetchEntityData !== null
116
+ ? refetchEntityData
117
+ : fragmentData
99
118
 
100
119
  const wrapLoad = <_Result>(
101
120
  setLoading: (val: boolean) => void,
@@ -120,7 +139,19 @@ export function useFragmentHandle<
120
139
  if (!refetchArtifact?.refetch?.paginated || !paginationObserver) return null
121
140
 
122
141
  const fetchFn: FetchFn<_Data, _Input> = (args) => {
123
- return paginationObserver.send({ ...args, session })
142
+ return paginationObserver.send({
143
+ ...args,
144
+ session,
145
+ stuff: { silenceLoading: true },
146
+ cacheParams: {
147
+ disableSubscriptions: true,
148
+ // Suppress partial cache hits so an in-flight forward navigation never
149
+ // briefly resolves with an entity that is missing its connection field.
150
+ // Full cache hits (partial: false) still resolve, so backward navigation
151
+ // continues to be served instantly from cache.
152
+ disablePartial: true,
153
+ },
154
+ })
124
155
  }
125
156
 
126
157
  const fetchUpdate = (args: any, updates: string[]) => {
@@ -139,8 +170,6 @@ export function useFragmentHandle<
139
170
  const handlers = cursorHandlers<_Data, _Input>({
140
171
  artifact: refetchArtifact,
141
172
  getState: () => displayData as _Data | null,
142
- // Use the observer's own variable state so cursor history is preserved
143
- // across page navigations without manual tracking in the hook.
144
173
  getVariables: () =>
145
174
  (paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
146
175
  fetch: fetchFn,
@@ -183,9 +212,48 @@ export function useFragmentHandle<
183
212
  return null
184
213
  }, [refetchArtifact, paginationObserver, displayData, session, forwardPending, backwardPending])
185
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
+
186
253
  return {
187
254
  ...handle,
188
- variables,
255
+ variables: displayVariables,
189
256
  data: displayData,
257
+ refetch,
190
258
  }
191
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: _Input
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
- >({ artifact }: { artifact: SubscriptionArtifact }, variables: _Input) {
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
  }
@@ -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,