Skip to content

Commit

Permalink
More React hooks and RuntimeScalars (#1280)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlecAivazis authored Mar 7, 2024
1 parent 50a9fa1 commit 4e31fbb
Show file tree
Hide file tree
Showing 45 changed files with 767 additions and 181 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-apes-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini-react': patch
---

Add useLocation hook
5 changes: 5 additions & 0 deletions .changeset/hungry-steaks-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

Add experimental support for RuntimeScalars
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare namespace App {
// user-specific information passed to each query
interface Session {}
}
1 change: 1 addition & 0 deletions packages/houdini-react/src/plugin/codegen/typeRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export async function writeTsconfig(config: Config) {
'../src/**/*.ts',
'../src/**/*.jsx',
'../src/**/*.tsx',
'../src/+app.d.ts',
],
exclude: ['../node_modules/**', './[!ambient.d.ts]**'],
},
Expand Down
73 changes: 39 additions & 34 deletions packages/houdini-react/src/plugin/vite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,42 +357,47 @@ if (window.__houdini__nav_caches__ && window.__houdini__nav_caches__.artifact_ca
request.headers.set(key, value as string)
}

// instantiate the handler and invoke it with a mocked response
const result: Response = await createServerAdapter({
production: false,
manifest: router_manifest,
assetPrefix: '/virtual:houdini',
pipe: res,
documentPremable: `<script type="module" src="/@vite/client" async=""></script>`,
})(request)
if (result && result.status === 404) {
return next()
}
// TODO: this is so awkward....
// if we got here but we didn't pipe a response then we have to send the result to the end
// by default result is a Response
if (result && typeof result !== 'boolean') {
if (res.closed) {
return
}
for (const header of Object.entries(result.headers ?? {})) {
res.setHeader(header[0], header[1])
try {
// instantiate the handler and invoke it with a mocked response
const result: Response = await createServerAdapter({
production: false,
manifest: router_manifest,
assetPrefix: '/virtual:houdini',
pipe: res,
documentPremable: `<script type="module" src="/@vite/client" async=""></script>`,
})(request)
if (result && result.status === 404) {
return next()
}
// handle redirects
if (result.status >= 300 && result.status < 400) {
res.writeHead(result.status, {
Location: result.headers.get('Location') ?? '',
...[...result.headers.entries()].reduce(
(headers, [key, value]) => ({
...headers,
[key]: value,
}),
{}
),
})
} else {
res.write(await result.text())
// TODO: this is so awkward....
// if we got here but we didn't pipe a response then we have to send the result to the end
// by default result is a Response
if (result && typeof result !== 'boolean') {
if (res.closed) {
return
}
for (const header of result.headers ?? []) {
res.setHeader(header[0], header[1])
}
// handle redirects
if (result.status >= 300 && result.status < 400) {
res.writeHead(result.status, {
Location: result.headers.get('Location') ?? '',
...[...result.headers].reduce(
(headers, [key, value]) => ({
...headers,
[key]: value,
}),
{}
),
})
} else {
res.write(await result.text())
}
res.end()
}
} catch (e) {
console.error(e)
res.end()
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function useDocumentHandle<
const [backwardPending, setBackwardPending] = React.useState(false)

// grab the current session value
const session = useSession()
const [session] = useSession()

// @ts-expect-error: avoiding an as DocumentHandle<_Artifact, _Data, _Input>
return React.useMemo<DocumentHandle<_Artifact, _Data, _Input>>(() => {
Expand Down
12 changes: 9 additions & 3 deletions packages/houdini-react/src/runtime/hooks/useDocumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export function useDocumentStore<
...observeParams
}: UseDocumentStoreParams<_Artifact, _Data, _Input>): [
QueryResult<_Data, _Input>,
DocumentStore<_Data, _Input>,
(store: DocumentStore<_Data, _Input>) => void
DocumentStore<_Data, _Input>
] {
const client = useClient()
const isMountedRef = useIsMountedRef()
Expand All @@ -43,6 +42,13 @@ export function useDocumentStore<

const box = React.useRef(observer.state)

// if the observer changes, we need to track the new one
if (obs && obs !== observer) {
box.current = obs.state
setObserver(obs)
}

// the function that registers a new subscription for the observer
const subscribe: any = React.useCallback(
(fn: () => void) => {
return observer.subscribe((val) => {
Expand All @@ -62,5 +68,5 @@ export function useDocumentStore<
() => box.current
)

return [storeValue!, observer, setObserver]
return [storeValue!, observer]
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,14 @@ export function useDocumentSubscription<
variables: _Input
disabled?: boolean
send?: Partial<SendParams>
}): [
QueryResult<_Data, _Input> & { parent?: string | null },
DocumentStore<_Data, _Input>,
(store: DocumentStore<_Data, _Input>) => void
] {
const [storeValue, observer, setObserver] = useDocumentStore<_Data, _Input>({
}): [QueryResult<_Data, _Input> & { parent?: string | null }, DocumentStore<_Data, _Input>] {
const [storeValue, observer] = useDocumentStore<_Data, _Input>({
artifact,
...observeParams,
})

// grab the current session value
const session = useSession()
const [session] = useSession()

// whenever the variables change, we need to retrigger the query
useDeepCompareEffect(() => {
Expand All @@ -58,6 +54,5 @@ export function useDocumentSubscription<
...storeValue,
},
observer,
setObserver,
]
}
2 changes: 1 addition & 1 deletion packages/houdini-react/src/runtime/hooks/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function useMutation<
const pending = storeValue.fetching

// grab the current session value
const session = useSession()
const [session] = useSession()

// sending the mutation just means invoking the observer's send method
const mutate: MutationHandler<_Result, _Input, _Optimistic> = ({
Expand Down
2 changes: 1 addition & 1 deletion packages/houdini-react/src/runtime/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import manifest from './manifest'
import { Router as RouterImpl, RouterCache, RouterContextProvider } from './routing'

export * from './hooks'
export { router_cache } from './routing'
export { router_cache, useSession, useLocation } from './routing'

export function Router({
cache,
Expand Down
49 changes: 45 additions & 4 deletions packages/houdini-react/src/runtime/routing/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Cache } from '$houdini/runtime/cache/cache'
import { DocumentStore, HoudiniClient } from '$houdini/runtime/client'
import configFile from '$houdini/runtime/imports/config'
import { deepEquals } from '$houdini/runtime/lib/deepEquals'
import { LRUCache } from '$houdini/runtime/lib/lru'
import { GraphQLObject, GraphQLVariables } from '$houdini/runtime/lib/types'
import { QueryArtifact } from '$houdini/runtime/lib/types'
import { find_match } from '$houdini/runtime/router/match'
import type { RouterManifest, RouterPageManifest } from '$houdini/runtime/router/types'
import React from 'react'
import { useContext } from 'react'

import { useDocumentStore } from '../hooks/useDocumentStore'
import { SuspenseCache, suspense_cache } from './cache'
Expand Down Expand Up @@ -120,11 +122,16 @@ export function Router({
// its needs
return (
<VariableContext.Provider value={variables}>
<PageComponent url={currentURL} key={page.id} />
<LocationContext.Provider value={{ pathname: currentURL }}>
<PageComponent url={currentURL} key={page.id} />
</LocationContext.Provider>
</VariableContext.Provider>
)
}

// export the location information in context
export const useLocation = () => useContext(LocationContext)

/**
* usePageData is responsible for kicking off the network requests necessary to render the page.
* This includes loading the artifact, the component source, and any query results. This hook
Expand Down Expand Up @@ -157,7 +164,7 @@ function usePageData({
} = useRouterContext()

// grab the current session value
const session = useSession()
const [session] = useSession()

// the function to load a query using the cache references
function load_query({ id, artifact }: { id: string; artifact: QueryArtifact }): Promise<void> {
Expand Down Expand Up @@ -440,6 +447,7 @@ export function RouterContextProvider({
ssr_signals,
last_variables,
session,
setSession: (newSession) => setSession((old) => ({ ...old, ...newSession })),
}}
>
{children}
Expand Down Expand Up @@ -470,6 +478,9 @@ type RouterContext = {

// The current session
session: App.Session

// a function to call that sets the client-side session singletone
setSession: (newSession: Partial<App.Session>) => void
}

export type PendingCache = SuspenseCache<
Expand Down Expand Up @@ -505,8 +516,36 @@ export function updateLocalSession(session: App.Session) {
)
}

export function useSession() {
return useRouterContext().session
export function useSession(): [App.Session, (newSession: Partial<App.Session>) => void] {
const ctx = useRouterContext()

// when we update the session we have to do 2 things. (1) we have to update the local state
// that we will use on the client (2) we have to send a request to the server so that it
// can update the cookie that we use for the session
const updateSession = (newSession: Partial<App.Session>) => {
// clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
ctx.data_cache.clear()

// update the local state
ctx.setSession(newSession)

// figure out the url that we will use to send values to the server
const auth = configFile.router?.auth
if (!auth) {
return
}
const url = 'redirect' in auth ? auth.redirect : auth.url

fetch(url, {
method: 'POST',
body: JSON.stringify(newSession),
headers: {
'Content-Type': 'application/json',
},
})
}

return [ctx.session, updateSession]
}

export function useCurrentVariables(): GraphQLVariables {
Expand All @@ -515,6 +554,8 @@ export function useCurrentVariables(): GraphQLVariables {

const VariableContext = React.createContext<GraphQLVariables>(null)

const LocationContext = React.createContext<{ pathname: string }>({ pathname: '' })

export function useQueryResult<_Data extends GraphQLObject, _Input extends GraphQLVariables>(
name: string
): [_Data | null, DocumentStore<_Data, _Input>] {
Expand Down
3 changes: 2 additions & 1 deletion packages/houdini-svelte/src/plugin/artifactData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ describe('load', () => {
},
"types": {},
"defaults": {}
"defaults": {},
"runtimeScalars": {}
},
"policy": "CacheOrNetwork",
Expand Down
11 changes: 8 additions & 3 deletions packages/houdini-svelte/src/plugin/transforms/kit/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,12 +528,17 @@ function variable_function_for_query(
for (const definition of query.variableDefinitions ?? []) {
const unwrapped = unwrapType(page.config, definition.type)

// if the type is a runtime scalar, its optional
const runtime_scalar =
page.config.configFile.features?.runtimeScalars?.[unwrapped.type.name]

// we need to remember the definition if
// the argument to the operation is non-null
// the url param doesn't exist or does exist but is optional
if (
unwrapped.wrappers[unwrapped.wrappers.length - 1] === TypeWrapper.NonNull &&
!definition.defaultValue &&
!runtime_scalar &&
(!params[definition.variable.name.value] ||
params[definition.variable.name.value].optional)
) {
Expand Down Expand Up @@ -564,8 +569,8 @@ function variable_function_for_query(
AST.variableDeclarator(
AST.identifier('result'),
AST.objectExpression(
Object.entries(has_args).map(([arg, type]) =>
AST.objectProperty(
Object.entries(has_args).map(([arg, type]) => {
return AST.objectProperty(
AST.identifier(arg),
AST.callExpression(AST.identifier('parseScalar'), [
AST.identifier('config'),
Expand All @@ -579,7 +584,7 @@ function variable_function_for_query(
),
])
)
)
})
)
),
]),
Expand Down
25 changes: 24 additions & 1 deletion packages/houdini/src/codegen/generators/artifacts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,30 @@ export default function artifactGenerator(stats: {
// if the document has inputs describe their types in the artifact so we can
// marshal and unmarshal scalars
if (inputs && inputs.length > 0) {
artifact.input = inputObject(config, inputs)
// any runtime scalars will be registered on the argument definition
const runtimeScalars = inputs.reduce((prev, input) => {
const runtimeScalarDirective = input.directives?.find(
(directive) =>
directive.name.value === config.runtimeScalarDirective
)

// if there is no runtime scalar directive then we don't need to do anything
if (!runtimeScalarDirective) {
return prev
}

// there is a runtime scalar definition so keep track of the field
return {
...prev,
[input.variable.name.value]: (
runtimeScalarDirective.arguments?.find(
(arg) => arg.name.value === 'type'
)?.value as graphql.StringValueNode
)?.value,
}
}, {} as Record<string, string>)

artifact.input = inputObject(config, inputs, runtimeScalars)
}

// add the cache policy to query documents
Expand Down
4 changes: 3 additions & 1 deletion packages/houdini/src/codegen/generators/artifacts/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { InputObject } from '../../../runtime/lib/types'

export function inputObject(
config: Config,
inputs: readonly graphql.VariableDefinitionNode[]
inputs: readonly graphql.VariableDefinitionNode[],
runtimeScalars: Record<string, string>
): InputObject {
// make sure we don't define the same input type
const visitedTypes = new Set<string>()
Expand All @@ -33,6 +34,7 @@ export function inputObject(
: undefined,
}
}, {}),
runtimeScalars,
}

// walk through every type referenced and add it to the list
Expand Down
Loading

0 comments on commit 4e31fbb

Please sign in to comment.