From 4a1b93dcf5ac66742286a8d1cc161b20e4eabc3e Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:01:47 -0500 Subject: [PATCH 1/6] fix(solid-query): hydrate preloaded data correctly Signed-off-by: marbemac # Conflicts: # packages/solid-query/src/createBaseQuery.ts --- packages/solid-query/src/createBaseQuery.ts | 73 +++++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/solid-query/src/createBaseQuery.ts b/packages/solid-query/src/createBaseQuery.ts index 222d867aa7..fd73360c82 100644 --- a/packages/solid-query/src/createBaseQuery.ts +++ b/packages/solid-query/src/createBaseQuery.ts @@ -18,9 +18,11 @@ import type { CreateBaseQueryOptions } from './types' import type { Accessor } from 'solid-js' import type { QueryClient } from './QueryClient' import type { + Query, QueryKey, QueryObserver, QueryObserverResult, + QueryState, } from '@tanstack/query-core' function reconcileFn( @@ -40,6 +42,45 @@ function reconcileFn( return { ...result, data: newData } as typeof result } +type HydrateableQueryState = QueryObserverResult & + QueryState + +/** + * Solid's `onHydrated` functionality will silently "fail" (hydrate with an empty object) + * if the resource data is not serializable. + */ +const hydrateableObserverResult = < + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey, + T2, +>( + query: Query, + result: QueryObserverResult, +): HydrateableQueryState => { + const { refetch, ...rest } = unwrap(result) + + return { + ...rest, + + // cast to refetch function should be safe, since we only remove it on the server, + // and refetch is not relevant on the server + refetch: isServer ? undefined : refetch, + + // hydrate() expects a QueryState object, which is similar but not + // quite the same as a QueryObserverResult object. Thus, for now, we're + // copying over the missing properties from state in order to support hydration + dataUpdateCount: query.state.dataUpdateCount, + fetchFailureCount: query.state.fetchFailureCount, + isInvalidated: query.state.isInvalidated, + + // Unsetting these properties on the server since they might not be serializable + fetchFailureReason: isServer ? null : query.state.fetchFailureReason, + fetchMeta: isServer ? null : query.state.fetchMeta, + } +} + // Base Query Function that is used to create the query. export function createBaseQuery< TQueryFnData, @@ -54,6 +95,10 @@ export function createBaseQuery< Observer: typeof QueryObserver, queryClient?: Accessor, ) { + type ResourceData = + | HydrateableQueryState + | QueryObserverResult + const client = createMemo(() => useQueryClient(queryClient?.())) const defaultedOptions = client().defaultQueryOptions(options()) @@ -71,30 +116,14 @@ export function createBaseQuery< const createServerSubscriber = ( resolve: ( - data: - | QueryObserverResult - | PromiseLike | undefined> - | undefined, + data: ResourceData | PromiseLike | undefined, ) => void, reject: (reason?: any) => void, ) => { return observer.subscribe((result) => { notifyManager.batchCalls(() => { const query = observer.getCurrentQuery() - const { refetch, ...rest } = unwrap(result) - const unwrappedResult = { - ...rest, - - // hydrate() expects a QueryState object, which is similar but not - // quite the same as a QueryObserverResult object. Thus, for now, we're - // copying over the missing properties from state in order to support hydration - dataUpdateCount: query.state.dataUpdateCount, - fetchFailureCount: query.state.fetchFailureCount, - // Removing these properties since they might not be serializable - // fetchFailureReason: query.state.fetchFailureReason, - // fetchMeta: query.state.fetchMeta, - isInvalidated: query.state.isInvalidated, - } + const unwrappedResult = hydrateableObserverResult(query, result) if (unwrappedResult.isError) { if (process.env['NODE_ENV'] === 'development') { @@ -105,7 +134,7 @@ export function createBaseQuery< if (unwrappedResult.isSuccess) { // Use of any here is fine // We cannot include refetch since it is not serializable - resolve(unwrappedResult as any) + resolve(unwrappedResult) } })() }) @@ -148,7 +177,7 @@ export function createBaseQuery< let unsubscribe: (() => void) | null = null const [queryResource, { refetch, mutate }] = createResource< - QueryObserverResult | undefined + ResourceData | undefined >( () => { return new Promise((resolve, reject) => { @@ -159,8 +188,10 @@ export function createBaseQuery< unsubscribe = createClientSubscriber() } } + if (!state.isLoading) { - resolve(state) + const query = observer.getCurrentQuery() + resolve(hydrateableObserverResult(query, state)) } }) }, From 86a60b02acaf43fb9ef92dc5d6d7af9634ad18a1 Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:33:12 -0500 Subject: [PATCH 2/6] only include extra props on server Signed-off-by: marbemac # Conflicts: # packages/solid-query/src/createBaseQuery.ts --- packages/solid-query/src/createBaseQuery.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/solid-query/src/createBaseQuery.ts b/packages/solid-query/src/createBaseQuery.ts index fd73360c82..45c7afd720 100644 --- a/packages/solid-query/src/createBaseQuery.ts +++ b/packages/solid-query/src/createBaseQuery.ts @@ -59,14 +59,18 @@ const hydrateableObserverResult = < query: Query, result: QueryObserverResult, ): HydrateableQueryState => { - const { refetch, ...rest } = unwrap(result) + // Including the extra properties is only relevant on the server + if (!isServer) return result as HydrateableQueryState return { - ...rest, + ...unwrap(result), // cast to refetch function should be safe, since we only remove it on the server, // and refetch is not relevant on the server - refetch: isServer ? undefined : refetch, + refetch: undefined as unknown as HydrateableQueryState< + T2, + TError + >['refetch'], // hydrate() expects a QueryState object, which is similar but not // quite the same as a QueryObserverResult object. Thus, for now, we're @@ -76,8 +80,8 @@ const hydrateableObserverResult = < isInvalidated: query.state.isInvalidated, // Unsetting these properties on the server since they might not be serializable - fetchFailureReason: isServer ? null : query.state.fetchFailureReason, - fetchMeta: isServer ? null : query.state.fetchMeta, + fetchFailureReason: null, + fetchMeta: null, } } From b41b0d82eb0fc6dd7f8da77105d501f17a619ea4 Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:33:24 -0500 Subject: [PATCH 3/6] fix test watch command Signed-off-by: marbemac --- packages/solid-query/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solid-query/package.json b/packages/solid-query/package.json index 0e3bcd69f3..3b37735510 100644 --- a/packages/solid-query/package.json +++ b/packages/solid-query/package.json @@ -43,7 +43,7 @@ "test:eslint": "eslint --ext .ts,.tsx ./src", "test:types": "tsc", "test:lib": "vitest run --coverage", - "test:lib:dev": "pnpm run test:lib --watch", + "test:lib:dev": "vitest watch --coverage", "test:build": "publint --strict", "build": "tsup" }, From 347f6f2d2230f9e2d0156c483bfb275157d23976 Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:34:08 -0500 Subject: [PATCH 4/6] update solid-start-streaming example deps Signed-off-by: marbemac # Conflicts: # examples/solid/solid-start-streaming/package.json # pnpm-lock.yaml --- examples/solid/solid-start-streaming/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/solid/solid-start-streaming/package.json b/examples/solid/solid-start-streaming/package.json index 38623c7be7..b611b265cb 100644 --- a/examples/solid/solid-start-streaming/package.json +++ b/examples/solid/solid-start-streaming/package.json @@ -7,6 +7,9 @@ "start": "solid-start start" }, "type": "module", + "resolutions": { + "solid-js": "^1.7.7" + }, "dependencies": { "@solidjs/meta": "^0.28.2", "@solidjs/router": "^0.7.0", From 5aae9adbf58fea9b2a4203d3071ca88c6d0e80ec Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:34:45 -0500 Subject: [PATCH 5/6] add prefetch example to solid-start-streaming Signed-off-by: marbemac --- .../src/components/user-info.tsx | 22 ++++++++---- .../solid/solid-start-streaming/src/root.tsx | 4 ++- .../src/routes/prefetch.tsx | 36 +++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 examples/solid/solid-start-streaming/src/routes/prefetch.tsx diff --git a/examples/solid/solid-start-streaming/src/components/user-info.tsx b/examples/solid/solid-start-streaming/src/components/user-info.tsx index a9cc4d6292..1353cf8941 100644 --- a/examples/solid/solid-start-streaming/src/components/user-info.tsx +++ b/examples/solid/solid-start-streaming/src/components/user-info.tsx @@ -11,15 +11,18 @@ export interface UserInfoProps { simulateError?: boolean } +export const userInfoQueryOpts = (props?: UserInfoProps) => ({ + queryKey: ['user'], + queryFn: () => fetchUser(props), + deferStream: props?.deferStream, +}) + export const UserInfo: Component = (props) => { const [simulateError, setSimulateError] = createSignal(props.simulateError) - const query = createQuery(() => ({ - queryKey: ['user'], - queryFn: () => - fetchUser({ sleep: props.sleep, simulateError: simulateError() }), - deferStream: props.deferStream, - })) + const query = createQuery(() => + userInfoQueryOpts({ ...props, simulateError: simulateError() }), + ) return ( = (props) => {
id: {user.id}
name: {user.name}
queryTime: {user.queryTime}
+ )} diff --git a/examples/solid/solid-start-streaming/src/root.tsx b/examples/solid/solid-start-streaming/src/root.tsx index f99901f914..b5971906ec 100644 --- a/examples/solid/solid-start-streaming/src/root.tsx +++ b/examples/solid/solid-start-streaming/src/root.tsx @@ -21,6 +21,7 @@ export default function Root() { defaultOptions: { queries: { retry: false, + staleTime: 5000, }, }, }) @@ -38,12 +39,13 @@ export default function Root() { loading... [root.tsx suspense boundary]} > - Index + Home Streamed Deferred Mixed With Error Hydration + Prefetch diff --git a/examples/solid/solid-start-streaming/src/routes/prefetch.tsx b/examples/solid/solid-start-streaming/src/routes/prefetch.tsx new file mode 100644 index 0000000000..376a9743dd --- /dev/null +++ b/examples/solid/solid-start-streaming/src/routes/prefetch.tsx @@ -0,0 +1,36 @@ +import { useQueryClient } from '@tanstack/solid-query' +import { isServer } from 'solid-js/web' +import { Title } from 'solid-start' +import { UserInfo, userInfoQueryOpts } from '~/components/user-info' + +export default function Prefetch() { + const queryClient = useQueryClient() + + if (isServer) { + void queryClient.prefetchQuery(userInfoQueryOpts({ sleep: 500 })) + } + + return ( +
+ Solid Query - Prefetch + +

Solid Query - Prefetch Example

+ +
+

+ In some cases you may want to prefetch a query on the server before + the component with the relevant `createQuery` call is mounted. A major + use case for this is in router data loaders, in order to avoid request + waterfalls. +

+

+ In this example we prefetch the user query (on the server only). There + should be no extra `fetchUser.start` and `fetchUser.done` logs in the + console on the client when refreshing the page. +

+
+ + +
+ ) +} From d79aef95a3edee40985f01944f2cce721305fbcd Mon Sep 17 00:00:00 2001 From: marbemac Date: Mon, 10 Jul 2023 12:36:02 -0500 Subject: [PATCH 6/6] fix: always fall back to resolving Signed-off-by: marbemac --- packages/solid-query/src/createBaseQuery.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/solid-query/src/createBaseQuery.ts b/packages/solid-query/src/createBaseQuery.ts index 45c7afd720..917ca8b728 100644 --- a/packages/solid-query/src/createBaseQuery.ts +++ b/packages/solid-query/src/createBaseQuery.ts @@ -130,14 +130,8 @@ export function createBaseQuery< const unwrappedResult = hydrateableObserverResult(query, result) if (unwrappedResult.isError) { - if (process.env['NODE_ENV'] === 'development') { - console.error(unwrappedResult.error) - } reject(unwrappedResult.error) - } - if (unwrappedResult.isSuccess) { - // Use of any here is fine - // We cannot include refetch since it is not serializable + } else { resolve(unwrappedResult) } })()