houdini-react 2.0.0-next.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/houdini-react +173 -0
- package/package.json +63 -39
- package/postInstall.js +360 -0
- package/runtime/Link.tsx +50 -0
- package/runtime/client.ts +5 -0
- package/runtime/clientPlugin.ts +17 -0
- package/runtime/componentFields.ts +79 -0
- package/runtime/escape.ts +15 -0
- package/runtime/hooks/index.ts +17 -0
- package/runtime/hooks/recycleNodesInto.ts +60 -0
- package/runtime/hooks/useDeepCompareEffect.ts +93 -0
- package/runtime/hooks/useDocumentHandle.ts +243 -0
- package/runtime/hooks/useDocumentStore.ts +112 -0
- package/runtime/hooks/useDocumentSubscription.ts +65 -0
- package/runtime/hooks/useFragment.ts +269 -0
- package/runtime/hooks/useFragmentHandle.ts +274 -0
- package/runtime/hooks/useIsMounted.ts +14 -0
- package/runtime/hooks/useLogoutForm.tsx +60 -0
- package/runtime/hooks/useMutation.ts +80 -0
- package/runtime/hooks/useMutationForm.tsx +163 -0
- package/runtime/hooks/useQuery.ts +12 -0
- package/runtime/hooks/useQueryHandle.ts +194 -0
- package/runtime/hooks/useSubscription.ts +12 -0
- package/runtime/hooks/useSubscriptionHandle.ts +42 -0
- package/runtime/hydration.tsx +186 -0
- package/runtime/index.tsx +77 -0
- package/runtime/login.ts +22 -0
- package/runtime/manifest.ts +6 -0
- package/runtime/mock.ts +9 -0
- package/runtime/package.json +1 -0
- package/runtime/resolve-href.ts +151 -0
- package/runtime/routes.ts +93 -0
- package/runtime/routing/Router.tsx +1101 -0
- package/runtime/routing/cache.ts +57 -0
- package/runtime/routing/errors.tsx +145 -0
- package/runtime/routing/index.ts +17 -0
- package/runtime/testing.tsx +163 -0
- package/runtime/tsconfig.json +39 -0
- package/server/index.d.ts +1 -0
- package/server/index.js +4 -0
- package/server/react-streaming.d.js +0 -0
- package/vite/index.d.ts +3 -0
- package/vite/index.js +397 -0
- package/vite/strip-headers.d.ts +1 -0
- package/vite/strip-headers.js +35 -0
- package/vite/transform.d.ts +13 -0
- package/vite/transform.js +105 -0
- package/README.md +0 -36
- package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
- package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
- package/build/plugin/codegen/entries/index.d.ts +0 -16
- package/build/plugin/codegen/entries/pages.d.ts +0 -2
- package/build/plugin/codegen/index.d.ts +0 -17
- package/build/plugin/codegen/manifest.d.ts +0 -5
- package/build/plugin/codegen/render.d.ts +0 -7
- package/build/plugin/codegen/router.d.ts +0 -7
- package/build/plugin/codegen/typeRoot.d.ts +0 -5
- package/build/plugin/config.d.ts +0 -4
- package/build/plugin/dedent.d.ts +0 -1
- package/build/plugin/extract.d.ts +0 -6
- package/build/plugin/index.d.ts +0 -5
- package/build/plugin/state.d.ts +0 -3
- package/build/plugin/transform.d.ts +0 -6
- package/build/plugin/vite.d.ts +0 -27
- package/build/plugin-cjs/index.js +0 -90119
- package/build/plugin-cjs/package.json +0 -1
- package/build/plugin-esm/index.js +0 -90115
- package/build/runtime/client.d.ts +0 -3
- package/build/runtime/clientPlugin.d.ts +0 -3
- package/build/runtime/componentFields.d.ts +0 -9
- package/build/runtime/hooks/index.d.ts +0 -8
- package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime/hooks/useFragment.d.ts +0 -16
- package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime/hooks/useMutation.d.ts +0 -14
- package/build/runtime/hooks/useQuery.d.ts +0 -5
- package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime/hooks/useSubscription.d.ts +0 -4
- package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime/index.d.ts +0 -14
- package/build/runtime/manifest.d.ts +0 -3
- package/build/runtime/routing/Router.d.ts +0 -62
- package/build/runtime/routing/cache.d.ts +0 -7
- package/build/runtime/routing/hooks.d.ts +0 -40
- package/build/runtime/routing/index.d.ts +0 -3
- package/build/runtime-cjs/client.d.ts +0 -3
- package/build/runtime-cjs/client.js +0 -25
- package/build/runtime-cjs/clientPlugin.d.ts +0 -3
- package/build/runtime-cjs/clientPlugin.js +0 -37
- package/build/runtime-cjs/componentFields.d.ts +0 -9
- package/build/runtime-cjs/componentFields.js +0 -83
- package/build/runtime-cjs/hooks/index.d.ts +0 -8
- package/build/runtime-cjs/hooks/index.js +0 -45
- package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
- package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
- package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
- package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
- package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
- package/build/runtime-cjs/hooks/useFragment.js +0 -102
- package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
- package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
- package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
- package/build/runtime-cjs/hooks/useMutation.js +0 -67
- package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
- package/build/runtime-cjs/hooks/useQuery.js +0 -32
- package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
- package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
- package/build/runtime-cjs/hooks/useSubscription.js +0 -32
- package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
- package/build/runtime-cjs/index.d.ts +0 -14
- package/build/runtime-cjs/index.js +0 -88
- package/build/runtime-cjs/manifest.d.ts +0 -3
- package/build/runtime-cjs/manifest.js +0 -25
- package/build/runtime-cjs/package.json +0 -1
- package/build/runtime-cjs/routing/Router.d.ts +0 -62
- package/build/runtime-cjs/routing/Router.js +0 -540
- package/build/runtime-cjs/routing/cache.d.ts +0 -7
- package/build/runtime-cjs/routing/cache.js +0 -61
- package/build/runtime-cjs/routing/hooks.d.ts +0 -40
- package/build/runtime-cjs/routing/hooks.js +0 -93
- package/build/runtime-cjs/routing/index.d.ts +0 -3
- package/build/runtime-cjs/routing/index.js +0 -33
- package/build/runtime-esm/client.d.ts +0 -3
- package/build/runtime-esm/client.js +0 -5
- package/build/runtime-esm/clientPlugin.d.ts +0 -3
- package/build/runtime-esm/clientPlugin.js +0 -17
- package/build/runtime-esm/componentFields.d.ts +0 -9
- package/build/runtime-esm/componentFields.js +0 -59
- package/build/runtime-esm/hooks/index.d.ts +0 -8
- package/build/runtime-esm/hooks/index.js +0 -15
- package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
- package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
- package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
- package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
- package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
- package/build/runtime-esm/hooks/useFragment.js +0 -67
- package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
- package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime-esm/hooks/useIsMounted.js +0 -14
- package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
- package/build/runtime-esm/hooks/useMutation.js +0 -42
- package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
- package/build/runtime-esm/hooks/useQuery.js +0 -8
- package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
- package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
- package/build/runtime-esm/hooks/useSubscription.js +0 -8
- package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
- package/build/runtime-esm/index.d.ts +0 -14
- package/build/runtime-esm/index.js +0 -48
- package/build/runtime-esm/manifest.d.ts +0 -3
- package/build/runtime-esm/manifest.js +0 -5
- package/build/runtime-esm/routing/Router.d.ts +0 -62
- package/build/runtime-esm/routing/Router.js +0 -499
- package/build/runtime-esm/routing/cache.d.ts +0 -7
- package/build/runtime-esm/routing/cache.js +0 -36
- package/build/runtime-esm/routing/hooks.d.ts +0 -40
- package/build/runtime-esm/routing/hooks.js +0 -53
- package/build/runtime-esm/routing/index.d.ts +0 -3
- package/build/runtime-esm/routing/index.js +0 -6
- package/build/server/index.d.ts +0 -1
- package/build/server-cjs/index.js +0 -28
- package/build/server-cjs/package.json +0 -1
- package/build/server-esm/index.js +0 -4
- package/build/server-esm/package.json +0 -1
- /package/{build/plugin-esm → server}/package.json +0 -0
- /package/{build/runtime-esm → vite}/package.json +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentArtifact,
|
|
3
|
+
GraphQLVariables,
|
|
4
|
+
QueryResult,
|
|
5
|
+
GraphQLObject,
|
|
6
|
+
} from 'houdini/runtime'
|
|
7
|
+
import type { DocumentStore, SendParams } from 'houdini/runtime/client'
|
|
8
|
+
|
|
9
|
+
import { useSession } from '../routing/Router.js'
|
|
10
|
+
import useDeepCompareEffect from './useDeepCompareEffect.js'
|
|
11
|
+
import { useDocumentStore, type UseDocumentStoreParams } from './useDocumentStore.js'
|
|
12
|
+
|
|
13
|
+
export function useDocumentSubscription<
|
|
14
|
+
_Artifact extends DocumentArtifact = DocumentArtifact,
|
|
15
|
+
_Data extends GraphQLObject = GraphQLObject,
|
|
16
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
17
|
+
>({
|
|
18
|
+
artifact,
|
|
19
|
+
variables,
|
|
20
|
+
send,
|
|
21
|
+
initialState,
|
|
22
|
+
disabled,
|
|
23
|
+
...observeParams
|
|
24
|
+
}: UseDocumentStoreParams<_Artifact, _Data, _Input> & {
|
|
25
|
+
variables: _Input
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
send?: Partial<Omit<SendParams, 'initialState'>>
|
|
28
|
+
}): [QueryResult<_Data, _Input> & { parent?: string | null }, DocumentStore<_Data, _Input>] {
|
|
29
|
+
const [storeValue, observer] = useDocumentStore<_Data, _Input>({
|
|
30
|
+
artifact,
|
|
31
|
+
initialState,
|
|
32
|
+
...observeParams,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// grab the current session value
|
|
36
|
+
const [session] = useSession()
|
|
37
|
+
|
|
38
|
+
// whenever the variables change, we need to retrigger the query
|
|
39
|
+
useDeepCompareEffect(() => {
|
|
40
|
+
if (!disabled) {
|
|
41
|
+
observer.send({
|
|
42
|
+
variables,
|
|
43
|
+
session,
|
|
44
|
+
// TODO: metadata
|
|
45
|
+
metadata: {},
|
|
46
|
+
...send,
|
|
47
|
+
initialState,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
if (!disabled) {
|
|
53
|
+
observer.cleanup()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, [disabled, session, observer, variables ?? {}, send ?? {}])
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
parent: send?.stuff?.parentID,
|
|
61
|
+
...storeValue,
|
|
62
|
+
},
|
|
63
|
+
observer,
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { fragmentKey } from 'houdini/runtime'
|
|
2
|
+
import type {
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
GraphQLVariables,
|
|
5
|
+
FragmentArtifact,
|
|
6
|
+
QueryResult,
|
|
7
|
+
SubscriptionSpec,
|
|
8
|
+
} from 'houdini/runtime'
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
|
|
11
|
+
import { useRouterContext } from '../routing/index.js'
|
|
12
|
+
import { useDocumentSubscription } from './useDocumentSubscription.js'
|
|
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
|
|
37
|
+
export function useFragment<
|
|
38
|
+
_Data extends GraphQLObject,
|
|
39
|
+
_ReferenceType extends {},
|
|
40
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
41
|
+
>(
|
|
42
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
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 }
|
|
73
|
+
): _Data | null {
|
|
74
|
+
const { cache } = useRouterContext()
|
|
75
|
+
const { parent, variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
|
|
76
|
+
reference,
|
|
77
|
+
document
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Read from cache whenever the parent or loading state changes. The parent
|
|
81
|
+
// path uniquely identifies which cache record this fragment is bound to, so
|
|
82
|
+
// variables are excluded from the dep array — they are forwarded to
|
|
83
|
+
// observer.send() separately and don't affect which record we read.
|
|
84
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: variables intentionally excluded
|
|
85
|
+
const cachedValue = React.useMemo(() => {
|
|
86
|
+
if (reference && parent) {
|
|
87
|
+
return cache.read({
|
|
88
|
+
selection: document.artifact.selection,
|
|
89
|
+
parent,
|
|
90
|
+
variables,
|
|
91
|
+
loading,
|
|
92
|
+
}).data as _Data
|
|
93
|
+
}
|
|
94
|
+
return reference as _Data | null
|
|
95
|
+
}, [parent, loading])
|
|
96
|
+
|
|
97
|
+
// Stable initialState derived from cachedValue. useDocumentStore uses this to
|
|
98
|
+
// seed box.current synchronously during render when the parent changes, so
|
|
99
|
+
// storeValue.data is immediately correct without waiting for the subscription
|
|
100
|
+
// effect to fire. Must be memoized (reference-stable) so the store doesn't
|
|
101
|
+
// re-seed on every render.
|
|
102
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: variables changes don't require re-seeding
|
|
103
|
+
const initialState = React.useMemo(
|
|
104
|
+
(): QueryResult<_Data, _Input> | undefined =>
|
|
105
|
+
cachedValue !== null
|
|
106
|
+
? {
|
|
107
|
+
data: cachedValue,
|
|
108
|
+
errors: null,
|
|
109
|
+
fetching: false,
|
|
110
|
+
partial: false,
|
|
111
|
+
stale: false,
|
|
112
|
+
source: null,
|
|
113
|
+
variables: variables ?? null,
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
116
|
+
[cachedValue]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const [storeValue] = useDocumentSubscription<FragmentArtifact, _Data, _Input>({
|
|
120
|
+
artifact: document.artifact,
|
|
121
|
+
variables,
|
|
122
|
+
initialValue: cachedValue,
|
|
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,
|
|
126
|
+
send: {
|
|
127
|
+
stuff: {
|
|
128
|
+
parentID: parent,
|
|
129
|
+
},
|
|
130
|
+
setup: true,
|
|
131
|
+
},
|
|
132
|
+
initialState,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return storeValue.data
|
|
136
|
+
}
|
|
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
|
+
|
|
251
|
+
export function fragmentReference<_Data extends GraphQLObject, _Input, _ReferenceType extends {}>(
|
|
252
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
253
|
+
document: { artifact: FragmentArtifact }
|
|
254
|
+
): { variables: _Input; parent: string; loading: boolean } {
|
|
255
|
+
// @ts-expect-error: typescript can't guarantee that the fragment key is defined
|
|
256
|
+
// but if its not, then the fragment wasn't mixed into the right thing
|
|
257
|
+
// the variables for the fragment live on the initial value's $fragment key
|
|
258
|
+
const { variables, parent } = reference?.[fragmentKey]?.values?.[document.artifact.name] ?? {}
|
|
259
|
+
if (reference && fragmentKey in reference && (!variables || !parent)) {
|
|
260
|
+
console.warn(
|
|
261
|
+
`⚠️ Parent does not contain the information for this fragment. Something is wrong.
|
|
262
|
+
Please ensure that you have passed a record that has ${document.artifact.name} mixed into it.`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
// @ts-expect-error
|
|
266
|
+
const loading = Boolean(reference?.[fragmentKey]?.loading)
|
|
267
|
+
|
|
268
|
+
return { variables, parent, loading }
|
|
269
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractPageInfo,
|
|
3
|
+
cursorHandlers,
|
|
4
|
+
offsetHandlers,
|
|
5
|
+
getCurrentConfig,
|
|
6
|
+
entityRefetchVariables,
|
|
7
|
+
} from 'houdini/runtime'
|
|
8
|
+
import type {
|
|
9
|
+
GraphQLObject,
|
|
10
|
+
FragmentArtifact,
|
|
11
|
+
QueryArtifact,
|
|
12
|
+
GraphQLVariables,
|
|
13
|
+
FetchFn,
|
|
14
|
+
} from 'houdini/runtime'
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
|
|
17
|
+
import { useClient, useSession } from '../routing/Router.js'
|
|
18
|
+
import type { DocumentHandle } from './useDocumentHandle.js'
|
|
19
|
+
import { fragmentReference, useFragment } from './useFragment.js'
|
|
20
|
+
|
|
21
|
+
// useFragmentHandle is just like useFragment except it also returns an imperative handle
|
|
22
|
+
// that users can use to interact with the fragment
|
|
23
|
+
export function useFragmentHandle<
|
|
24
|
+
_Artifact extends FragmentArtifact,
|
|
25
|
+
_Data extends GraphQLObject,
|
|
26
|
+
_ReferenceType extends {},
|
|
27
|
+
_PaginationArtifact extends QueryArtifact,
|
|
28
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
29
|
+
>(
|
|
30
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
31
|
+
document: { artifact: FragmentArtifact; refetchArtifact?: QueryArtifact }
|
|
32
|
+
): any {
|
|
33
|
+
const fragmentData = useFragment<_Data, _ReferenceType, _Input>(reference, document)
|
|
34
|
+
const { variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
|
|
35
|
+
reference,
|
|
36
|
+
document
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const client = useClient()
|
|
40
|
+
const [session] = useSession()
|
|
41
|
+
|
|
42
|
+
const [forwardPending, setForwardPending] = React.useState(false)
|
|
43
|
+
const [backwardPending, setBackwardPending] = React.useState(false)
|
|
44
|
+
|
|
45
|
+
const previousCursorsRef = React.useRef<(string | null)[]>([])
|
|
46
|
+
const nextCursorsRef = React.useRef<(string | null)[]>([])
|
|
47
|
+
|
|
48
|
+
const refetchArtifact = document.refetchArtifact as QueryArtifact | undefined
|
|
49
|
+
const refetchPath = refetchArtifact?.refetch?.path
|
|
50
|
+
|
|
51
|
+
const paginationObserver = React.useMemo(() => {
|
|
52
|
+
if (!refetchArtifact?.refetch?.paginated) return null
|
|
53
|
+
return client.observe<_Data, _Input>({ artifact: refetchArtifact })
|
|
54
|
+
}, [refetchArtifact?.name])
|
|
55
|
+
|
|
56
|
+
const isSinglePage = refetchArtifact?.refetch?.mode === 'SinglePage'
|
|
57
|
+
|
|
58
|
+
// Subscribe to the pagination observer so the component re-renders when a new page lands.
|
|
59
|
+
// For SinglePage we pass disablePartial so partial cache hits (entity found but connection
|
|
60
|
+
// not yet fetched) are never resolved back to the observer — we go straight to the network
|
|
61
|
+
// and update the observer only once we have a complete page.
|
|
62
|
+
const subscribeToObserver = React.useCallback(
|
|
63
|
+
(fn: () => void) => paginationObserver?.subscribe(() => fn()) ?? (() => {}),
|
|
64
|
+
[paginationObserver]
|
|
65
|
+
)
|
|
66
|
+
const getObserverSnapshot = React.useCallback(
|
|
67
|
+
() => paginationObserver?.state.data ?? null,
|
|
68
|
+
[paginationObserver]
|
|
69
|
+
)
|
|
70
|
+
const paginationData = React.useSyncExternalStore(
|
|
71
|
+
subscribeToObserver,
|
|
72
|
+
getObserverSnapshot,
|
|
73
|
+
getObserverSnapshot
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const paginationEntityData = React.useMemo<_Data | null>(() => {
|
|
77
|
+
if (!paginationData || !refetchArtifact?.selection?.fields) return null
|
|
78
|
+
const rootField = Object.keys(refetchArtifact.selection.fields)[0]
|
|
79
|
+
if (!rootField) return null
|
|
80
|
+
return (paginationData as any)[rootField] ?? null
|
|
81
|
+
}, [paginationData, refetchArtifact])
|
|
82
|
+
|
|
83
|
+
// @refetchable fragments embed the fragment in a query keyed by id (paginated: false).
|
|
84
|
+
// We observe that query so refetch() can swap in fresh data computed with new argument
|
|
85
|
+
// values, just like SinglePage pagination swaps in the latest page.
|
|
86
|
+
const isRefetchable = !!refetchArtifact?.refetch && !refetchArtifact.refetch.paginated
|
|
87
|
+
|
|
88
|
+
const refetchObserver = React.useMemo(() => {
|
|
89
|
+
if (!isRefetchable || !refetchArtifact) return null
|
|
90
|
+
return client.observe<_Data, _Input>({ artifact: refetchArtifact })
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
}, [refetchArtifact?.name, isRefetchable])
|
|
93
|
+
|
|
94
|
+
const subscribeToRefetch = React.useCallback(
|
|
95
|
+
(fn: () => void) => refetchObserver?.subscribe(() => fn()) ?? (() => {}),
|
|
96
|
+
[refetchObserver]
|
|
97
|
+
)
|
|
98
|
+
const getRefetchSnapshot = React.useCallback(
|
|
99
|
+
() => refetchObserver?.state.data ?? null,
|
|
100
|
+
[refetchObserver]
|
|
101
|
+
)
|
|
102
|
+
const refetchData = React.useSyncExternalStore(
|
|
103
|
+
subscribeToRefetch,
|
|
104
|
+
getRefetchSnapshot,
|
|
105
|
+
getRefetchSnapshot
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const refetchEntityData = React.useMemo<_Data | null>(() => {
|
|
109
|
+
if (!refetchData || !refetchArtifact?.selection?.fields) return null
|
|
110
|
+
const rootField = Object.keys(refetchArtifact.selection.fields)[0]
|
|
111
|
+
if (!rootField) return null
|
|
112
|
+
return (refetchData as any)[rootField] ?? null
|
|
113
|
+
}, [refetchData, refetchArtifact])
|
|
114
|
+
|
|
115
|
+
const displayData =
|
|
116
|
+
isSinglePage && paginationEntityData !== null
|
|
117
|
+
? paginationEntityData
|
|
118
|
+
: isRefetchable && refetchEntityData !== null
|
|
119
|
+
? refetchEntityData
|
|
120
|
+
: fragmentData
|
|
121
|
+
|
|
122
|
+
const wrapLoad = <_Result>(
|
|
123
|
+
setLoading: (val: boolean) => void,
|
|
124
|
+
fn: (value: any) => Promise<_Result>
|
|
125
|
+
) => {
|
|
126
|
+
return async (value: any) => {
|
|
127
|
+
setLoading(true)
|
|
128
|
+
let err: Error | null = null
|
|
129
|
+
let result: _Result | null = null
|
|
130
|
+
try {
|
|
131
|
+
result = await fn(value)
|
|
132
|
+
} catch (e) {
|
|
133
|
+
err = e as Error
|
|
134
|
+
}
|
|
135
|
+
setLoading(false)
|
|
136
|
+
if (err && err.name !== 'AbortError') throw err
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handle = React.useMemo(() => {
|
|
142
|
+
if (!refetchArtifact?.refetch?.paginated || !paginationObserver) return null
|
|
143
|
+
|
|
144
|
+
const fetchFn: FetchFn<_Data, _Input> = (args) => {
|
|
145
|
+
return paginationObserver.send({
|
|
146
|
+
...args,
|
|
147
|
+
session,
|
|
148
|
+
stuff: { silenceLoading: true },
|
|
149
|
+
cacheParams: {
|
|
150
|
+
disableSubscriptions: true,
|
|
151
|
+
// Suppress partial cache hits so an in-flight forward navigation never
|
|
152
|
+
// briefly resolves with an entity that is missing its connection field.
|
|
153
|
+
// Full cache hits (partial: false) still resolve, so backward navigation
|
|
154
|
+
// continues to be served instantly from cache.
|
|
155
|
+
disablePartial: true,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fetchUpdate = (args: any, updates: string[]) => {
|
|
161
|
+
return paginationObserver.send({
|
|
162
|
+
...args,
|
|
163
|
+
cacheParams: {
|
|
164
|
+
...args?.cacheParams,
|
|
165
|
+
disableSubscriptions: true,
|
|
166
|
+
applyUpdates: updates,
|
|
167
|
+
},
|
|
168
|
+
session,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (refetchArtifact.refetch!.method === 'cursor') {
|
|
173
|
+
const handlers = cursorHandlers<_Data, _Input>({
|
|
174
|
+
artifact: refetchArtifact,
|
|
175
|
+
getState: () => displayData as _Data | null,
|
|
176
|
+
getVariables: () =>
|
|
177
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
178
|
+
// no-op pagination while the parent is still in its @loading state (issue #1408)
|
|
179
|
+
getLoading: () => loading,
|
|
180
|
+
fetch: fetchFn,
|
|
181
|
+
fetchUpdate,
|
|
182
|
+
getSession: async () => session,
|
|
183
|
+
previousCursors: previousCursorsRef.current,
|
|
184
|
+
nextCursors: nextCursorsRef.current,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
189
|
+
loadNextPending: forwardPending,
|
|
190
|
+
loadPrevious: wrapLoad(setBackwardPending, handlers.loadPreviousPage),
|
|
191
|
+
loadPreviousPending: backwardPending,
|
|
192
|
+
pageInfo: refetchPath
|
|
193
|
+
? extractPageInfo(displayData as GraphQLObject, refetchPath)
|
|
194
|
+
: null,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (refetchArtifact.refetch!.method === 'offset') {
|
|
199
|
+
const handlers = offsetHandlers({
|
|
200
|
+
artifact: refetchArtifact,
|
|
201
|
+
getState: () => displayData as _Data | null,
|
|
202
|
+
getVariables: () =>
|
|
203
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
204
|
+
// no-op pagination while the parent is still in its @loading state (issue #1408)
|
|
205
|
+
getLoading: () => loading,
|
|
206
|
+
storeName: refetchArtifact.name,
|
|
207
|
+
fetch: fetchFn,
|
|
208
|
+
fetchUpdate: async (args: any, updates = ['append']) =>
|
|
209
|
+
fetchUpdate(args, updates) as any,
|
|
210
|
+
getSession: async () => session,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
215
|
+
loadNextPending: forwardPending,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null
|
|
220
|
+
}, [
|
|
221
|
+
refetchArtifact,
|
|
222
|
+
paginationObserver,
|
|
223
|
+
displayData,
|
|
224
|
+
session,
|
|
225
|
+
forwardPending,
|
|
226
|
+
backwardPending,
|
|
227
|
+
loading,
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
// the fragment's current argument values: the initial args overlaid with everything that
|
|
231
|
+
// has been passed to refetch() so far. we track these explicitly rather than reading them
|
|
232
|
+
// back off the embedded query, whose variables also carry the synthetic id-lookup keys.
|
|
233
|
+
const [refetchArgs, setRefetchArgs] = React.useState<Partial<_Input>>({})
|
|
234
|
+
|
|
235
|
+
// re-run the embedded query with new argument values. the entity's id is derived from
|
|
236
|
+
// the fragment data (the parent reference), which always carries the visible id. we must
|
|
237
|
+
// NOT derive it from displayData: after a refetch that becomes the embedded query result,
|
|
238
|
+
// which masks the entity's id out of its selection, so reading it back would yield
|
|
239
|
+
// `id: undefined` and clobber the real id on a second refetch.
|
|
240
|
+
const refetch = React.useMemo(() => {
|
|
241
|
+
if (!isRefetchable || !refetchObserver || !refetchArtifact) return undefined
|
|
242
|
+
return (newVariables?: _Input) => {
|
|
243
|
+
setRefetchArgs((prev) => ({ ...prev, ...newVariables }))
|
|
244
|
+
const idVariables = entityRefetchVariables(
|
|
245
|
+
getCurrentConfig(),
|
|
246
|
+
refetchArtifact.refetch?.targetType,
|
|
247
|
+
fragmentData as Record<string, any> | null
|
|
248
|
+
)
|
|
249
|
+
return refetchObserver.send({
|
|
250
|
+
variables: {
|
|
251
|
+
...(refetchObserver.state.variables ?? variables),
|
|
252
|
+
...idVariables,
|
|
253
|
+
...newVariables,
|
|
254
|
+
} as _Input,
|
|
255
|
+
// suppress loading-state placeholder data during the transition so the
|
|
256
|
+
// currently displayed value stays put until the fresh result arrives
|
|
257
|
+
stuff: { silenceLoading: true },
|
|
258
|
+
cacheParams: { disableSubscriptions: true, disablePartial: true },
|
|
259
|
+
session,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}, [isRefetchable, refetchObserver, refetchArtifact, fragmentData, variables, session])
|
|
263
|
+
|
|
264
|
+
const displayVariables = isRefetchable
|
|
265
|
+
? ({ ...(variables as Record<string, any>), ...refetchArgs } as _Input)
|
|
266
|
+
: variables
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
...handle,
|
|
270
|
+
variables: displayVariables,
|
|
271
|
+
data: displayData,
|
|
272
|
+
refetch,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useIsMountedRef(): { current: boolean } {
|
|
4
|
+
const isMountedRef = useRef(true)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
isMountedRef.current = true
|
|
8
|
+
return () => {
|
|
9
|
+
isMountedRef.current = false
|
|
10
|
+
}
|
|
11
|
+
}, [])
|
|
12
|
+
|
|
13
|
+
return isMountedRef
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getAuthUrl } from 'houdini/runtime'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { useSession, useRoute } from '../routing/Router.js'
|
|
5
|
+
|
|
6
|
+
export type UseLogoutFormOptions = {
|
|
7
|
+
// where to navigate after logout. The no-JS path 303s here from the server; the enhanced
|
|
8
|
+
// path navigates with goto(). Defaults to '/'.
|
|
9
|
+
redirectTo?: string
|
|
10
|
+
// enhanced-path-only side effect (a no-op without JS, the correct degradation)
|
|
11
|
+
onSuccess?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LogoutForm = {
|
|
15
|
+
// spread onto a <form>: a real string action so the native POST clears the session before
|
|
16
|
+
// hydration; the onSubmit intercepts after. Identical server/client → clean hydration.
|
|
17
|
+
form: {
|
|
18
|
+
action: string
|
|
19
|
+
method: 'post'
|
|
20
|
+
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
|
21
|
+
}
|
|
22
|
+
// the hidden marker fields the no-JS POST needs; render inside the <form>
|
|
23
|
+
hidden: React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* useLogoutForm renders a progressively-enhanced logout form. Without JS the native POST hits
|
|
28
|
+
* the auth endpoint, which deletes the session cookie and 303s to `redirectTo`. After
|
|
29
|
+
* hydration the onSubmit clears the session client-side (local state + cookie) and navigates.
|
|
30
|
+
* It mirrors `useMutationForm`, but there's no mutation — logout is just "clear the session".
|
|
31
|
+
*/
|
|
32
|
+
export function useLogoutForm(opts: UseLogoutFormOptions = {}): LogoutForm {
|
|
33
|
+
const [, updateSession] = useSession()
|
|
34
|
+
const { goto } = useRoute()
|
|
35
|
+
const redirectTo = opts.redirectTo ?? '/'
|
|
36
|
+
|
|
37
|
+
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
38
|
+
// only reached after hydration — before that the browser submits natively
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
// null logs out: clears local state and deletes the cookie
|
|
41
|
+
await updateSession(null)
|
|
42
|
+
opts.onSuccess?.()
|
|
43
|
+
goto(redirectTo)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const form: LogoutForm['form'] = {
|
|
47
|
+
action: getAuthUrl(),
|
|
48
|
+
method: 'post',
|
|
49
|
+
onSubmit,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hidden = (
|
|
53
|
+
<>
|
|
54
|
+
<input type="hidden" name="__houdini_logout" value="1" />
|
|
55
|
+
<input type="hidden" name="redirectTo" value={redirectTo} />
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return { form, hidden }
|
|
60
|
+
}
|