houdini-react 2.0.0-next.9 → 2.0.1

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 (185) hide show
  1. package/bin/houdini-react +173 -0
  2. package/package.json +63 -39
  3. package/postInstall.js +360 -0
  4. package/runtime/Link.tsx +50 -0
  5. package/runtime/client.ts +5 -0
  6. package/runtime/clientPlugin.ts +17 -0
  7. package/runtime/componentFields.ts +79 -0
  8. package/runtime/escape.ts +15 -0
  9. package/runtime/hooks/index.ts +17 -0
  10. package/runtime/hooks/recycleNodesInto.ts +60 -0
  11. package/runtime/hooks/useDeepCompareEffect.ts +93 -0
  12. package/runtime/hooks/useDocumentHandle.ts +243 -0
  13. package/runtime/hooks/useDocumentStore.ts +112 -0
  14. package/runtime/hooks/useDocumentSubscription.ts +65 -0
  15. package/runtime/hooks/useFragment.ts +269 -0
  16. package/runtime/hooks/useFragmentHandle.ts +274 -0
  17. package/runtime/hooks/useIsMounted.ts +14 -0
  18. package/runtime/hooks/useLogoutForm.tsx +60 -0
  19. package/runtime/hooks/useMutation.ts +80 -0
  20. package/runtime/hooks/useMutationForm.tsx +163 -0
  21. package/runtime/hooks/useQuery.ts +12 -0
  22. package/runtime/hooks/useQueryHandle.ts +194 -0
  23. package/runtime/hooks/useSubscription.ts +12 -0
  24. package/runtime/hooks/useSubscriptionHandle.ts +42 -0
  25. package/runtime/hydration.tsx +186 -0
  26. package/runtime/index.tsx +77 -0
  27. package/runtime/login.ts +22 -0
  28. package/runtime/manifest.ts +6 -0
  29. package/runtime/mock.ts +9 -0
  30. package/runtime/package.json +1 -0
  31. package/runtime/resolve-href.ts +151 -0
  32. package/runtime/routes.ts +93 -0
  33. package/runtime/routing/Router.tsx +1101 -0
  34. package/runtime/routing/cache.ts +57 -0
  35. package/runtime/routing/errors.tsx +145 -0
  36. package/runtime/routing/index.ts +17 -0
  37. package/runtime/testing.tsx +163 -0
  38. package/runtime/tsconfig.json +39 -0
  39. package/server/index.d.ts +1 -0
  40. package/server/index.js +4 -0
  41. package/server/react-streaming.d.js +0 -0
  42. package/vite/index.d.ts +3 -0
  43. package/vite/index.js +397 -0
  44. package/vite/strip-headers.d.ts +1 -0
  45. package/vite/strip-headers.js +35 -0
  46. package/vite/transform.d.ts +13 -0
  47. package/vite/transform.js +105 -0
  48. package/README.md +0 -36
  49. package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
  50. package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
  51. package/build/plugin/codegen/entries/index.d.ts +0 -16
  52. package/build/plugin/codegen/entries/pages.d.ts +0 -2
  53. package/build/plugin/codegen/index.d.ts +0 -17
  54. package/build/plugin/codegen/manifest.d.ts +0 -5
  55. package/build/plugin/codegen/render.d.ts +0 -7
  56. package/build/plugin/codegen/router.d.ts +0 -7
  57. package/build/plugin/codegen/typeRoot.d.ts +0 -5
  58. package/build/plugin/config.d.ts +0 -4
  59. package/build/plugin/dedent.d.ts +0 -1
  60. package/build/plugin/extract.d.ts +0 -6
  61. package/build/plugin/index.d.ts +0 -5
  62. package/build/plugin/state.d.ts +0 -3
  63. package/build/plugin/transform.d.ts +0 -6
  64. package/build/plugin/vite.d.ts +0 -27
  65. package/build/plugin-cjs/index.js +0 -90119
  66. package/build/plugin-cjs/package.json +0 -1
  67. package/build/plugin-esm/index.js +0 -90115
  68. package/build/runtime/client.d.ts +0 -3
  69. package/build/runtime/clientPlugin.d.ts +0 -3
  70. package/build/runtime/componentFields.d.ts +0 -9
  71. package/build/runtime/hooks/index.d.ts +0 -8
  72. package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
  73. package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
  74. package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
  75. package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
  76. package/build/runtime/hooks/useFragment.d.ts +0 -16
  77. package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
  78. package/build/runtime/hooks/useIsMounted.d.ts +0 -3
  79. package/build/runtime/hooks/useMutation.d.ts +0 -14
  80. package/build/runtime/hooks/useQuery.d.ts +0 -5
  81. package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
  82. package/build/runtime/hooks/useSubscription.d.ts +0 -4
  83. package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
  84. package/build/runtime/index.d.ts +0 -14
  85. package/build/runtime/manifest.d.ts +0 -3
  86. package/build/runtime/routing/Router.d.ts +0 -62
  87. package/build/runtime/routing/cache.d.ts +0 -7
  88. package/build/runtime/routing/hooks.d.ts +0 -40
  89. package/build/runtime/routing/index.d.ts +0 -3
  90. package/build/runtime-cjs/client.d.ts +0 -3
  91. package/build/runtime-cjs/client.js +0 -25
  92. package/build/runtime-cjs/clientPlugin.d.ts +0 -3
  93. package/build/runtime-cjs/clientPlugin.js +0 -37
  94. package/build/runtime-cjs/componentFields.d.ts +0 -9
  95. package/build/runtime-cjs/componentFields.js +0 -83
  96. package/build/runtime-cjs/hooks/index.d.ts +0 -8
  97. package/build/runtime-cjs/hooks/index.js +0 -45
  98. package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
  99. package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
  100. package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
  101. package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
  102. package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
  103. package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
  104. package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
  105. package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
  106. package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
  107. package/build/runtime-cjs/hooks/useFragment.js +0 -102
  108. package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
  109. package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
  110. package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
  111. package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
  112. package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
  113. package/build/runtime-cjs/hooks/useMutation.js +0 -67
  114. package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
  115. package/build/runtime-cjs/hooks/useQuery.js +0 -32
  116. package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
  117. package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
  118. package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
  119. package/build/runtime-cjs/hooks/useSubscription.js +0 -32
  120. package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
  121. package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
  122. package/build/runtime-cjs/index.d.ts +0 -14
  123. package/build/runtime-cjs/index.js +0 -88
  124. package/build/runtime-cjs/manifest.d.ts +0 -3
  125. package/build/runtime-cjs/manifest.js +0 -25
  126. package/build/runtime-cjs/package.json +0 -1
  127. package/build/runtime-cjs/routing/Router.d.ts +0 -62
  128. package/build/runtime-cjs/routing/Router.js +0 -540
  129. package/build/runtime-cjs/routing/cache.d.ts +0 -7
  130. package/build/runtime-cjs/routing/cache.js +0 -61
  131. package/build/runtime-cjs/routing/hooks.d.ts +0 -40
  132. package/build/runtime-cjs/routing/hooks.js +0 -93
  133. package/build/runtime-cjs/routing/index.d.ts +0 -3
  134. package/build/runtime-cjs/routing/index.js +0 -33
  135. package/build/runtime-esm/client.d.ts +0 -3
  136. package/build/runtime-esm/client.js +0 -5
  137. package/build/runtime-esm/clientPlugin.d.ts +0 -3
  138. package/build/runtime-esm/clientPlugin.js +0 -17
  139. package/build/runtime-esm/componentFields.d.ts +0 -9
  140. package/build/runtime-esm/componentFields.js +0 -59
  141. package/build/runtime-esm/hooks/index.d.ts +0 -8
  142. package/build/runtime-esm/hooks/index.js +0 -15
  143. package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
  144. package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
  145. package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
  146. package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
  147. package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
  148. package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
  149. package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
  150. package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
  151. package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
  152. package/build/runtime-esm/hooks/useFragment.js +0 -67
  153. package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
  154. package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
  155. package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
  156. package/build/runtime-esm/hooks/useIsMounted.js +0 -14
  157. package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
  158. package/build/runtime-esm/hooks/useMutation.js +0 -42
  159. package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
  160. package/build/runtime-esm/hooks/useQuery.js +0 -8
  161. package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
  162. package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
  163. package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
  164. package/build/runtime-esm/hooks/useSubscription.js +0 -8
  165. package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
  166. package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
  167. package/build/runtime-esm/index.d.ts +0 -14
  168. package/build/runtime-esm/index.js +0 -48
  169. package/build/runtime-esm/manifest.d.ts +0 -3
  170. package/build/runtime-esm/manifest.js +0 -5
  171. package/build/runtime-esm/routing/Router.d.ts +0 -62
  172. package/build/runtime-esm/routing/Router.js +0 -499
  173. package/build/runtime-esm/routing/cache.d.ts +0 -7
  174. package/build/runtime-esm/routing/cache.js +0 -36
  175. package/build/runtime-esm/routing/hooks.d.ts +0 -40
  176. package/build/runtime-esm/routing/hooks.js +0 -53
  177. package/build/runtime-esm/routing/index.d.ts +0 -3
  178. package/build/runtime-esm/routing/index.js +0 -6
  179. package/build/server/index.d.ts +0 -1
  180. package/build/server-cjs/index.js +0 -28
  181. package/build/server-cjs/package.json +0 -1
  182. package/build/server-esm/index.js +0 -4
  183. package/build/server-esm/package.json +0 -1
  184. /package/{build/plugin-esm → server}/package.json +0 -0
  185. /package/{build/runtime-esm → vite}/package.json +0 -0
@@ -0,0 +1,50 @@
1
+ // this file is generated by houdini — do not edit
2
+ // @refresh reset
3
+ import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
4
+ import { getCurrentConfig } from '$houdini/runtime/config'
5
+ import React from 'react'
6
+
7
+ // @ts-ignore
8
+ import manifest from './manifest.js'
9
+ import type { RouteHrefs, ExternalHref, ParamsForRoute, SearchForRoute } from './routes.js'
10
+
11
+ import { buildHref, type RouteHrefInfo } from './resolve-href.js'
12
+
13
+ // re-exported for custom link wrappers that constrain their own `to` prop
14
+ export type { RouteHrefs }
15
+
16
+ export type LinkProps<H extends RouteHrefs | ExternalHref = RouteHrefs | ExternalHref> = Omit<
17
+ DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
18
+ 'href'
19
+ > & {
20
+ to: H
21
+ disabled?: boolean
22
+ preload?: boolean | 'data' | 'component' | 'page'
23
+ } & ParamsForRoute<H> &
24
+ SearchForRoute<H>
25
+
26
+ export function Link<H extends RouteHrefs | ExternalHref>({
27
+ to,
28
+ params,
29
+ search,
30
+ disabled,
31
+ preload,
32
+ ...rest
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
40
+ const href = disabled
41
+ ? undefined
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
+ )
49
+ return React.createElement('a', { ...rest, href, 'data-houdini-preload': preload })
50
+ }
@@ -0,0 +1,5 @@
1
+ import type { HoudiniClient } from '$houdini/runtime/client'
2
+
3
+ // this file will be replaced by the code generation process
4
+
5
+ export default (() => {}) as () => HoudiniClient
@@ -0,0 +1,17 @@
1
+ import type { ClientPlugin } from 'houdini/runtime/client'
2
+
3
+ const plugin: () => ClientPlugin = () => () => {
4
+ return {
5
+ start(ctx, { next }) {
6
+ next({
7
+ ...ctx,
8
+ cacheParams: {
9
+ ...ctx.cacheParams,
10
+ serverSideFallback: false,
11
+ },
12
+ })
13
+ },
14
+ }
15
+ }
16
+
17
+ export default plugin
@@ -0,0 +1,79 @@
1
+ import { getFieldsForType } from 'houdini/runtime'
2
+ import type { DocumentArtifact, GraphQLObject, GraphQLValue } from 'houdini/runtime'
3
+ import { defaultComponentField, type Cache } from 'houdini/runtime/cache'
4
+
5
+ export function injectComponents({
6
+ cache,
7
+ selection,
8
+ data,
9
+ variables,
10
+ parentType = 'Query',
11
+ }: {
12
+ cache: Cache
13
+ selection: DocumentArtifact['selection']
14
+ data: GraphQLValue | null
15
+ variables: Record<string, GraphQLValue> | undefined | null
16
+ parentType?: string
17
+ }) {
18
+ // if the value is null, we're done
19
+ if (data === null) {
20
+ return
21
+ }
22
+
23
+ // if the value is not an object (ie its a scalar) we're done
24
+ if (typeof data !== 'object') {
25
+ return
26
+ }
27
+ data = data as GraphQLObject
28
+
29
+ // if the value is an array we need to instantiate each item
30
+ if (Array.isArray(data)) {
31
+ data.forEach((item) => {
32
+ injectComponents({
33
+ cache,
34
+ selection,
35
+ data: item,
36
+ variables,
37
+ parentType,
38
+ })
39
+ })
40
+ return
41
+ }
42
+
43
+ // if the object has a subselection we need to walk down
44
+ const typename = '__typename' in data ? (data.__typename as string) : ''
45
+ const fields = getFieldsForType(selection, typename, false)
46
+ if (!fields) {
47
+ return
48
+ }
49
+
50
+ // walk down each field
51
+ for (const [field, subSelection] of Object.entries(fields)) {
52
+ // if the field is a component then we need to assign the value to the target
53
+ if (subSelection.component) {
54
+ // if the component is already in the cache, we're done
55
+ if (!cache._internal_unstable.componentCache[subSelection.component.key]) {
56
+ continue
57
+ }
58
+
59
+ data[field] = defaultComponentField({
60
+ variables,
61
+ parent: cache._internal_unstable.id(parentType, data) ?? '',
62
+ cache,
63
+ component: subSelection.component,
64
+ }) as any as GraphQLValue
65
+ }
66
+
67
+ // if there is a selection, we need to walk down
68
+ const dataValue = data[field]
69
+ if (subSelection.selection) {
70
+ injectComponents({
71
+ cache,
72
+ selection: subSelection.selection,
73
+ data: dataValue,
74
+ variables,
75
+ parentType: subSelection.type,
76
+ })
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,15 @@
1
+ // escapeScriptTag makes an already-serialized JSON string safe to embed inside an inline
2
+ // <script>. JSON.stringify does not escape these, so without it a value that echoes
3
+ // attacker input (e.g. a form result on the error re-render) could break out of the script
4
+ // tag -- a reflected XSS. We escape:
5
+ // - "<" -> neutralizes </script> and <!-- breakouts
6
+ // - U+2028 / U+2029 -> JS line terminators that would otherwise end the inline script
7
+ const LS = String.fromCharCode(0x2028)
8
+ const PS = String.fromCharCode(0x2029)
9
+
10
+ export function escapeScriptTag(json: string): string {
11
+ return json
12
+ .replace(/</g, '\\u003c')
13
+ .replace(new RegExp(LS, 'g'), '\\u2028')
14
+ .replace(new RegExp(PS, 'g'), '\\u2029')
15
+ }
@@ -0,0 +1,17 @@
1
+ export { useQuery } from './useQuery.js'
2
+ export { useQueryHandle } from './useQueryHandle.js'
3
+ export { useFragment } from './useFragment.js'
4
+ export { useFragmentHandle } from './useFragmentHandle.js'
5
+ export { useMutation } from './useMutation.js'
6
+ export { useMutationForm, useMutationFormStatus } from './useMutationForm.js'
7
+ export { useLogoutForm } from './useLogoutForm.js'
8
+ export { useSubscription } from './useSubscription.js'
9
+
10
+ export type { DocumentHandle } from './useDocumentHandle.js'
11
+ export type { UseQueryConfig } from './useQueryHandle.js'
12
+ export type {
13
+ MutationForm,
14
+ MutationFormState,
15
+ UseMutationFormOptions,
16
+ } from './useMutationForm.js'
17
+ export type { LogoutForm, UseLogoutFormOptions } from './useLogoutForm.js'
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Walk `next` and, wherever a sub-tree is deeply equal to the corresponding
3
+ * sub-tree in `prev`, substitute `prev`'s reference. This preserves object
4
+ * identity for unchanged branches so React.memo can bail out on re-renders
5
+ * when a cache write touches unrelated parts of the graph.
6
+ *
7
+ * Arrays are reconciled by index. When lengths differ the array itself gets a
8
+ * new reference, but individual matching elements are still recycled so that
9
+ * items from the previous render keep their identities.
10
+ */
11
+ export function recycleNodesInto<T>(prev: T | null | undefined, next: T): T {
12
+ if (Object.is(prev, next)) return prev as T
13
+
14
+ if (next === null || typeof next !== 'object') return next
15
+ if (prev === null || prev === undefined || typeof prev !== 'object') return next
16
+
17
+ if (Array.isArray(next)) {
18
+ if (!Array.isArray(prev)) return next
19
+
20
+ const nextLen = next.length
21
+ const prevLen = (prev as unknown[]).length
22
+ const minLen = Math.min(prevLen, nextLen)
23
+
24
+ let changed = false
25
+ const result: unknown[] = new Array(nextLen)
26
+
27
+ for (let i = 0; i < minLen; i++) {
28
+ result[i] = recycleNodesInto((prev as unknown[])[i], next[i])
29
+ if (result[i] !== (prev as unknown[])[i]) changed = true
30
+ }
31
+ for (let i = minLen; i < nextLen; i++) {
32
+ result[i] = next[i]
33
+ }
34
+
35
+ if (!changed && prevLen === nextLen) return prev as T
36
+
37
+ // Copy non-index own properties from next (e.g. __id from @includeListID)
38
+ for (const key of Object.keys(next as any)) {
39
+ if (isNaN(Number(key))) {
40
+ ;(result as any)[key] = (next as any)[key]
41
+ }
42
+ }
43
+ return result as unknown as T
44
+ }
45
+
46
+ const prevObj = prev as Record<string, unknown>
47
+ const nextObj = next as Record<string, unknown>
48
+ const nextKeys = Object.keys(nextObj)
49
+ const prevKeyCount = Object.keys(prevObj).length
50
+
51
+ let changed = prevKeyCount !== nextKeys.length
52
+ const result: Record<string, unknown> = {}
53
+
54
+ for (const key of nextKeys) {
55
+ result[key] = recycleNodesInto(prevObj[key], nextObj[key])
56
+ if (result[key] !== prevObj[key]) changed = true
57
+ }
58
+
59
+ return changed ? (result as T) : (prev as T)
60
+ }
@@ -0,0 +1,93 @@
1
+ import { deepEquals } from 'houdini/runtime'
2
+ import * as React from 'react'
3
+
4
+ // This file is largely a copy and paste from Kent C. Dodd's use-deep-compare-effect (appropriate license at the bottom).
5
+ // It has been copied locally in order to avoid any awkward third party peer dependencies
6
+ // on generated files (which would make the install annoying). The deep equals library has
7
+ // also been changed to use one that was already included in the runtime (avoiding the extra bundle size)
8
+
9
+ type UseEffectParams = Parameters<typeof React.useEffect>
10
+ type EffectCallback = UseEffectParams[0]
11
+ type DependencyList = UseEffectParams[1]
12
+ // yes, I know it's void, but I like what this communicates about
13
+ // the intent of these functions: It's just like useEffect
14
+ type UseEffectReturn = ReturnType<typeof React.useEffect>
15
+
16
+ function checkDeps(deps: DependencyList) {
17
+ if (!deps?.length) {
18
+ throw new Error(
19
+ 'useDeepCompareEffect should not be used with no dependencies. Use React.useEffect instead.'
20
+ )
21
+ }
22
+ if (deps.every(isPrimitive)) {
23
+ throw new Error(
24
+ 'useDeepCompareEffect should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
25
+ )
26
+ }
27
+ }
28
+
29
+ function isPrimitive(val: unknown) {
30
+ return val == null || /^[sbn]/.test(typeof val)
31
+ }
32
+
33
+ /**
34
+ * @param value the value to be memoized (usually a dependency list)
35
+ * @returns a memoized version of the value as long as it remains deeply equal
36
+ */
37
+ export function useDeepCompareMemoize<T>(value: T) {
38
+ const ref = React.useRef<T>(value)
39
+
40
+ if (!deepEquals(value, ref.current)) {
41
+ ref.current = value
42
+ }
43
+
44
+ // Return ref.current directly so React.useEffect sees a new reference only
45
+ // when the deep value actually changes — matching the original use-deep-compare-effect.
46
+ // The useMemo(() => ref.current, []) wrapper that was here previously was incorrect:
47
+ // it froze the returned value at mount time, preventing effects from ever re-firing.
48
+ return ref.current
49
+ }
50
+
51
+ function useDeepCompareEffect(
52
+ callback: EffectCallback,
53
+ dependencies: DependencyList
54
+ ): UseEffectReturn {
55
+ if (process.env.NODE_ENV !== 'production') {
56
+ checkDeps(dependencies)
57
+ }
58
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional deep-compare effect
59
+ return React.useEffect(callback, useDeepCompareMemoize(dependencies))
60
+ }
61
+
62
+ export function useDeepCompareEffectNoCheck(
63
+ callback: EffectCallback,
64
+ dependencies: DependencyList
65
+ ): UseEffectReturn {
66
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional deep-compare effect
67
+ return React.useEffect(callback, useDeepCompareMemoize(dependencies))
68
+ }
69
+
70
+ export default useDeepCompareEffect
71
+
72
+ /**
73
+ The MIT License (MIT)
74
+ Copyright (c) 2020 Kent C. Dodds
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining a copy
77
+ of this software and associated documentation files (the "Software"), to deal
78
+ in the Software without restriction, including without limitation the rights
79
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
80
+ copies of the Software, and to permit persons to whom the Software is
81
+ furnished to do so, subject to the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be included in all
84
+ copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
87
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
88
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
89
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
90
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
91
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
92
+ SOFTWARE.
93
+ */
@@ -0,0 +1,243 @@
1
+ import { extractPageInfo } from 'houdini/runtime'
2
+ import { cursorHandlers, offsetHandlers } from 'houdini/runtime'
3
+ import { ArtifactKind } from 'houdini/runtime'
4
+ import type {
5
+ GraphQLObject,
6
+ GraphQLVariables,
7
+ GraphQLError,
8
+ CursorHandlers,
9
+ OffsetHandlers,
10
+ PageInfo,
11
+ FetchFn,
12
+ QueryResult,
13
+ DocumentArtifact,
14
+ QueryArtifact,
15
+ } from 'houdini/runtime'
16
+ import type { DocumentStore } from 'houdini/runtime/client'
17
+ import React from 'react'
18
+
19
+ import { useClient, useLocationContext, useSession } from '../routing/Router.js'
20
+
21
+ export function useDocumentHandle<
22
+ _Artifact extends QueryArtifact,
23
+ _Data extends GraphQLObject,
24
+ _Input extends GraphQLVariables,
25
+ >({
26
+ artifact,
27
+ observer,
28
+ storeValue,
29
+ }: {
30
+ artifact: DocumentArtifact
31
+ observer: DocumentStore<_Data, _Input>
32
+ storeValue: QueryResult<_Data, _Input>
33
+ }): DocumentHandle<_Artifact, _Data, _Input> & { fetch: FetchFn<_Data, _Input> } {
34
+ const [forwardPending, setForwardPending] = React.useState(false)
35
+ const [backwardPending, setBackwardPending] = React.useState(false)
36
+ // Stable cursor stacks for SinglePage pagination — must survive re-renders caused by store updates
37
+ const previousCursorsRef = React.useRef<(string | null)[]>([])
38
+ const nextCursorsRef = React.useRef<(string | null)[]>([])
39
+ const location = useLocationContext()
40
+
41
+ // grab the current session value
42
+ const [session] = useSession()
43
+
44
+ // we want to use a separate observer for pagination queries
45
+ const client = useClient()
46
+ const paginationObserver = React.useMemo(() => {
47
+ // if the artifact doesn't support pagination, don't do anything
48
+ if (!artifact.refetch?.paginated) {
49
+ return null
50
+ }
51
+
52
+ return client.observe<_Data, _Input>({ artifact })
53
+ }, [artifact.name, artifact, client.observe])
54
+
55
+ // @ts-expect-error: avoiding an as DocumentHandle<_Artifact, _Data, _Input>
56
+ return React.useMemo<DocumentHandle<_Artifact, _Data, _Input>>(() => {
57
+ const wrapLoad = <_Result>(
58
+ setLoading: (val: boolean) => void,
59
+ fn: (value: any) => Promise<_Result>
60
+ ) => {
61
+ return async (value: any) => {
62
+ setLoading(true)
63
+ let result: _Result | null = null
64
+ let err: Error | null = null
65
+ try {
66
+ result = await fn(value)
67
+ } catch (e) {
68
+ err = e as Error
69
+ }
70
+ setLoading(false)
71
+ // ignore abort errors when loading pages
72
+ if (err && err.name !== 'AbortError') {
73
+ throw err
74
+ }
75
+
76
+ // we're done
77
+ return result || observer.state
78
+ }
79
+ }
80
+
81
+ // add the session value to the
82
+ const fetchQuery: FetchFn<_Data, _Input> = (args) => {
83
+ // before we send the query, we need to figure out which variables are
84
+ // actually useful for this document
85
+ const usedVariables = Object.fromEntries(
86
+ Object.keys(observer.artifact.input?.fields ?? {}).reduce<[string, any][]>(
87
+ (entries, fieldName) => {
88
+ // if the field is not a url parameter, skip it
89
+ if (!(fieldName in location.params)) {
90
+ return entries
91
+ }
92
+
93
+ return [...entries, [fieldName, location.params[fieldName]]]
94
+ },
95
+ []
96
+ )
97
+ )
98
+
99
+ return observer.send({
100
+ ...args,
101
+ variables: {
102
+ ...usedVariables,
103
+ ...args?.variables,
104
+ },
105
+ session,
106
+ })
107
+ }
108
+
109
+ const baseFields = {
110
+ artifact,
111
+ data: storeValue.data,
112
+ variables: storeValue.variables,
113
+ partial: storeValue.partial,
114
+ fetching: storeValue.fetching,
115
+ errors: storeValue.errors,
116
+ }
117
+
118
+ // only consider paginated queries
119
+ if (artifact.kind !== ArtifactKind.Query || !artifact.refetch?.paginated) {
120
+ return {
121
+ ...baseFields,
122
+ fetch: fetchQuery,
123
+ }
124
+ }
125
+
126
+ // if the artifact supports cursor pagination, then add the cursor handlers
127
+ if (artifact.refetch.method === 'cursor') {
128
+ const handlers = cursorHandlers<_Data, _Input>({
129
+ artifact,
130
+ getState: () => storeValue.data,
131
+ getVariables: () => storeValue.variables!,
132
+ fetch: fetchQuery,
133
+ previousCursors: previousCursorsRef.current,
134
+ nextCursors: nextCursorsRef.current,
135
+ fetchUpdate: (args, updates) => {
136
+ return paginationObserver!.send({
137
+ ...args,
138
+ cacheParams: {
139
+ ...args?.cacheParams,
140
+ disableSubscriptions: true,
141
+ applyUpdates: updates,
142
+ },
143
+ session,
144
+ })
145
+ },
146
+ getSession: async () => session,
147
+ })
148
+
149
+ return {
150
+ ...baseFields,
151
+ fetch: handlers.fetch,
152
+ loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
153
+ loadNextPending: forwardPending,
154
+ loadPrevious: wrapLoad(setBackwardPending, handlers.loadPreviousPage),
155
+ loadPreviousPending: backwardPending,
156
+ pageInfo: extractPageInfo(storeValue.data, artifact.refetch!.path),
157
+ }
158
+ }
159
+
160
+ if (artifact.refetch.method === 'offset') {
161
+ const handlers = offsetHandlers({
162
+ artifact,
163
+ getState: () => storeValue.data,
164
+ getVariables: () => storeValue.variables!,
165
+ storeName: artifact.name,
166
+ fetch: fetchQuery,
167
+ fetchUpdate: async (args, updates = ['append']) => {
168
+ return paginationObserver!.send({
169
+ ...args,
170
+ cacheParams: {
171
+ disableSubscriptions: true,
172
+ applyUpdates: updates,
173
+ ...args?.cacheParams,
174
+ },
175
+ })
176
+ },
177
+ getSession: async () => session,
178
+ })
179
+
180
+ return {
181
+ ...baseFields,
182
+ fetch: handlers.fetch,
183
+ loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
184
+ loadNextPending: forwardPending,
185
+ }
186
+ }
187
+
188
+ // we don't want to add anything
189
+ return {
190
+ ...baseFields,
191
+ fetch: fetchQuery,
192
+ refetch: fetchQuery,
193
+ }
194
+ }, [
195
+ artifact,
196
+ observer,
197
+ session,
198
+ storeValue,
199
+ backwardPending,
200
+ forwardPending,
201
+ paginationObserver,
202
+ location.params,
203
+ ])
204
+ }
205
+
206
+ export type DocumentHandle<
207
+ _Artifact extends QueryArtifact,
208
+ _Data extends GraphQLObject = GraphQLObject,
209
+ _Input extends GraphQLVariables = GraphQLVariables,
210
+ > = {
211
+ data: _Data
212
+ partial: boolean
213
+ fetching: boolean
214
+ errors: GraphQLError[] | null
215
+ fetch: FetchFn<_Data, Partial<_Input>>
216
+ variables: _Input
217
+ } & RefetchHandlers<_Artifact, _Data, _Input>
218
+
219
+ type RefetchHandlers<_Artifact extends QueryArtifact, _Data extends GraphQLObject, _Input> =
220
+ // we need to add different methods if the artifact supports cursor pagination
221
+ _Artifact extends {
222
+ refetch: { paginated: true; method: 'cursor' }
223
+ }
224
+ ? {
225
+ loadNext: CursorHandlers<_Data, _Input>['loadNextPage']
226
+ loadNextPending: boolean
227
+ loadPrevious: CursorHandlers<_Data, _Input>['loadPreviousPage']
228
+ loadPreviousPending: boolean
229
+ pageInfo: PageInfo
230
+ }
231
+ : // offset pagination
232
+ _Artifact extends { refetch: { paginated: true; method: 'offset' } }
233
+ ? {
234
+ loadNext: OffsetHandlers<_Data, _Input>['loadNextPage']
235
+ loadNextPending: boolean
236
+ }
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
+ {}
@@ -0,0 +1,112 @@
1
+ import type {
2
+ DocumentArtifact,
3
+ GraphQLVariables,
4
+ QueryResult,
5
+ GraphQLObject,
6
+ } from 'houdini/runtime'
7
+ import type { DocumentStore, ObserveParams } from 'houdini/runtime/client'
8
+ import * as React from 'react'
9
+
10
+ import { useClient } from '../routing/index.js'
11
+ import { useIsMountedRef } from './useIsMounted.js'
12
+ import { recycleNodesInto } from './recycleNodesInto.js'
13
+
14
+ export type UseDocumentStoreParams<
15
+ _Artifact extends DocumentArtifact,
16
+ _Data extends GraphQLObject,
17
+ _Input extends GraphQLVariables,
18
+ > = {
19
+ artifact: _Artifact
20
+ observer?: DocumentStore<_Data, _Input>
21
+ // Optional synchronous seed for box.current. When provided, box.current is updated
22
+ // during render so useSyncExternalStore's snapshot is immediately correct (e.g. on
23
+ // fragment parent change). Must be memoized by the caller — tracked by reference.
24
+ initialState?: QueryResult<_Data, _Input>
25
+ } & Partial<ObserveParams<_Data, DocumentArtifact, _Input>>
26
+
27
+ export function useDocumentStore<
28
+ _Data extends GraphQLObject = GraphQLObject,
29
+ _Input extends GraphQLVariables = GraphQLVariables,
30
+ _Artifact extends DocumentArtifact = DocumentArtifact,
31
+ >({
32
+ artifact,
33
+ observer: obs,
34
+ initialState,
35
+ ...observeParams
36
+ }: UseDocumentStoreParams<_Artifact, _Data, _Input>): [
37
+ QueryResult<_Data, _Input>,
38
+ DocumentStore<_Data, _Input>,
39
+ ] {
40
+ const client = useClient()
41
+ const isMountedRef = useIsMountedRef()
42
+
43
+ // hold onto an observer we'll use
44
+ const [observer, setObserver] = React.useState(
45
+ () =>
46
+ obs ??
47
+ client.observe<_Data, _Input>({
48
+ artifact,
49
+ ...observeParams,
50
+ })
51
+ )
52
+
53
+ const box = React.useRef(observer.state)
54
+
55
+ // if the observer changes, we need to track the new one
56
+ if (obs && obs !== observer) {
57
+ box.current = obs.state
58
+ setObserver(obs)
59
+ }
60
+
61
+ // Relay-style synchronous seeding: when initialState changes (i.e., the fragment
62
+ // parent changed), update box.current immediately during this render so
63
+ // useSyncExternalStore's getSnapshot returns the correct data without waiting for
64
+ // the subscription effect to fire. Tracked by reference — if provided, callers
65
+ // must memoize initialState to avoid spurious reseeds on every render.
66
+ const prevInitialStateRef = React.useRef<QueryResult<_Data, _Input> | undefined>(undefined)
67
+ if (initialState !== undefined && initialState !== prevInitialStateRef.current) {
68
+ prevInitialStateRef.current = initialState
69
+ box.current = initialState
70
+ }
71
+
72
+ // the function that registers a new subscription for the observer
73
+ const subscribe: any = React.useCallback(
74
+ (fn: () => void) => {
75
+ return observer.subscribe((val) => {
76
+ const prev = box.current
77
+ // Preserve object identity for unchanged subtrees so React.memo on
78
+ // fragment components can bail out when their data wasn't touched.
79
+ const stableData = recycleNodesInto(prev?.data, val.data)
80
+ const next = stableData === val.data ? val : { ...val, data: stableData }
81
+
82
+ // Skip the re-render entirely if the new state is semantically identical
83
+ // to what React already has (e.g. an idempotent cache write).
84
+ if (
85
+ next === prev ||
86
+ (stableData === prev?.data &&
87
+ val.fetching === prev?.fetching &&
88
+ val.errors === prev?.errors &&
89
+ val.source === prev?.source &&
90
+ val.stale === prev?.stale)
91
+ ) {
92
+ return
93
+ }
94
+
95
+ box.current = next
96
+ if (isMountedRef.current) {
97
+ fn()
98
+ }
99
+ })
100
+ },
101
+ [observer, isMountedRef.current]
102
+ )
103
+
104
+ // get a safe reference to the cache
105
+ const storeValue = React.useSyncExternalStore(
106
+ subscribe,
107
+ () => box.current,
108
+ () => box.current
109
+ )
110
+
111
+ return [storeValue!, observer]
112
+ }