Skip to content

Commit 7bfecd6

Browse files
author
Marc MacLeod
authored
fix(solid-query): hydrate preloaded data correctly (#5775)
* fix(solid-query): hydrate preloaded data correctly Signed-off-by: marbemac <[email protected]> # Conflicts: # packages/solid-query/src/createBaseQuery.ts * only include extra props on server Signed-off-by: marbemac <[email protected]> # Conflicts: # packages/solid-query/src/createBaseQuery.ts * fix test watch command Signed-off-by: marbemac <[email protected]> * update solid-start-streaming example deps Signed-off-by: marbemac <[email protected]> # Conflicts: # examples/solid/solid-start-streaming/package.json # pnpm-lock.yaml * add prefetch example to solid-start-streaming Signed-off-by: marbemac <[email protected]> * fix: always fall back to resolving Signed-off-by: marbemac <[email protected]> --------- Signed-off-by: marbemac <[email protected]>
1 parent befad58 commit 7bfecd6

File tree

6 files changed

+116
-36
lines changed

6 files changed

+116
-36
lines changed

examples/solid/solid-start-streaming/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"start": "solid-start start"
88
},
99
"type": "module",
10+
"resolutions": {
11+
"solid-js": "^1.7.7"
12+
},
1013
"dependencies": {
1114
"@solidjs/meta": "^0.28.2",
1215
"@solidjs/router": "^0.7.0",

examples/solid/solid-start-streaming/src/components/user-info.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ export interface UserInfoProps {
1111
simulateError?: boolean
1212
}
1313

14+
export const userInfoQueryOpts = (props?: UserInfoProps) => ({
15+
queryKey: ['user'],
16+
queryFn: () => fetchUser(props),
17+
deferStream: props?.deferStream,
18+
})
19+
1420
export const UserInfo: Component<UserInfoProps> = (props) => {
1521
const [simulateError, setSimulateError] = createSignal(props.simulateError)
1622

17-
const query = createQuery(() => ({
18-
queryKey: ['user'],
19-
queryFn: () =>
20-
fetchUser({ sleep: props.sleep, simulateError: simulateError() }),
21-
deferStream: props.deferStream,
22-
}))
23+
const query = createQuery(() =>
24+
userInfoQueryOpts({ ...props, simulateError: simulateError() }),
25+
)
2326

2427
return (
2528
<Example
@@ -49,6 +52,13 @@ export const UserInfo: Component<UserInfoProps> = (props) => {
4952
<div>id: {user.id}</div>
5053
<div>name: {user.name}</div>
5154
<div>queryTime: {user.queryTime}</div>
55+
<button
56+
onClick={() => {
57+
query.refetch()
58+
}}
59+
>
60+
refetch
61+
</button>
5262
</>
5363
)}
5464
</QueryBoundary>

examples/solid/solid-start-streaming/src/root.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default function Root() {
2121
defaultOptions: {
2222
queries: {
2323
retry: false,
24+
staleTime: 5000,
2425
},
2526
},
2627
})
@@ -38,12 +39,13 @@ export default function Root() {
3839
<Suspense
3940
fallback={<div>loading... [root.tsx suspense boundary]</div>}
4041
>
41-
<A href="/">Index</A>
42+
<A href="/">Home</A>
4243
<A href="/streamed">Streamed</A>
4344
<A href="/deferred">Deferred</A>
4445
<A href="/mixed">Mixed</A>
4546
<A href="/with-error">With Error</A>
4647
<A href="/hydration">Hydration</A>
48+
<A href="/prefetch">Prefetch</A>
4749

4850
<Routes>
4951
<FileRoutes />
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useQueryClient } from '@tanstack/solid-query'
2+
import { isServer } from 'solid-js/web'
3+
import { Title } from 'solid-start'
4+
import { UserInfo, userInfoQueryOpts } from '~/components/user-info'
5+
6+
export default function Prefetch() {
7+
const queryClient = useQueryClient()
8+
9+
if (isServer) {
10+
void queryClient.prefetchQuery(userInfoQueryOpts({ sleep: 500 }))
11+
}
12+
13+
return (
14+
<main>
15+
<Title>Solid Query - Prefetch</Title>
16+
17+
<h1>Solid Query - Prefetch Example</h1>
18+
19+
<div class="description">
20+
<p>
21+
In some cases you may want to prefetch a query on the server before
22+
the component with the relevant `createQuery` call is mounted. A major
23+
use case for this is in router data loaders, in order to avoid request
24+
waterfalls.
25+
</p>
26+
<p>
27+
In this example we prefetch the user query (on the server only). There
28+
should be no extra `fetchUser.start` and `fetchUser.done` logs in the
29+
console on the client when refreshing the page.
30+
</p>
31+
</div>
32+
33+
<UserInfo sleep={500} deferStream />
34+
</main>
35+
)
36+
}

packages/solid-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"test:eslint": "eslint --ext .ts,.tsx ./src",
4444
"test:types": "tsc",
4545
"test:lib": "vitest run --coverage",
46-
"test:lib:dev": "pnpm run test:lib --watch",
46+
"test:lib:dev": "vitest watch --coverage",
4747
"test:build": "publint --strict",
4848
"build": "tsup"
4949
},

packages/solid-query/src/createBaseQuery.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import type { CreateBaseQueryOptions } from './types'
1818
import type { Accessor } from 'solid-js'
1919
import type { QueryClient } from './QueryClient'
2020
import type {
21+
Query,
2122
QueryKey,
2223
QueryObserver,
2324
QueryObserverResult,
25+
QueryState,
2426
} from '@tanstack/query-core'
2527

2628
function reconcileFn<TData, TError>(
@@ -40,6 +42,49 @@ function reconcileFn<TData, TError>(
4042
return { ...result, data: newData } as typeof result
4143
}
4244

45+
type HydrateableQueryState<TData, TError> = QueryObserverResult<TData, TError> &
46+
QueryState<TData, TError>
47+
48+
/**
49+
* Solid's `onHydrated` functionality will silently "fail" (hydrate with an empty object)
50+
* if the resource data is not serializable.
51+
*/
52+
const hydrateableObserverResult = <
53+
TQueryFnData,
54+
TError,
55+
TData,
56+
TQueryKey extends QueryKey,
57+
T2,
58+
>(
59+
query: Query<TQueryFnData, TError, TData, TQueryKey>,
60+
result: QueryObserverResult<T2, TError>,
61+
): HydrateableQueryState<T2, TError> => {
62+
// Including the extra properties is only relevant on the server
63+
if (!isServer) return result as HydrateableQueryState<T2, TError>
64+
65+
return {
66+
...unwrap(result),
67+
68+
// cast to refetch function should be safe, since we only remove it on the server,
69+
// and refetch is not relevant on the server
70+
refetch: undefined as unknown as HydrateableQueryState<
71+
T2,
72+
TError
73+
>['refetch'],
74+
75+
// hydrate() expects a QueryState object, which is similar but not
76+
// quite the same as a QueryObserverResult object. Thus, for now, we're
77+
// copying over the missing properties from state in order to support hydration
78+
dataUpdateCount: query.state.dataUpdateCount,
79+
fetchFailureCount: query.state.fetchFailureCount,
80+
isInvalidated: query.state.isInvalidated,
81+
82+
// Unsetting these properties on the server since they might not be serializable
83+
fetchFailureReason: null,
84+
fetchMeta: null,
85+
}
86+
}
87+
4388
// Base Query Function that is used to create the query.
4489
export function createBaseQuery<
4590
TQueryFnData,
@@ -54,6 +99,10 @@ export function createBaseQuery<
5499
Observer: typeof QueryObserver,
55100
queryClient?: Accessor<QueryClient>,
56101
) {
102+
type ResourceData =
103+
| HydrateableQueryState<TData, TError>
104+
| QueryObserverResult<TData, TError>
105+
57106
const client = createMemo(() => useQueryClient(queryClient?.()))
58107

59108
const defaultedOptions = client().defaultQueryOptions(options())
@@ -71,41 +120,19 @@ export function createBaseQuery<
71120

72121
const createServerSubscriber = (
73122
resolve: (
74-
data:
75-
| QueryObserverResult<TData, TError>
76-
| PromiseLike<QueryObserverResult<TData, TError> | undefined>
77-
| undefined,
123+
data: ResourceData | PromiseLike<ResourceData | undefined> | undefined,
78124
) => void,
79125
reject: (reason?: any) => void,
80126
) => {
81127
return observer.subscribe((result) => {
82128
notifyManager.batchCalls(() => {
83129
const query = observer.getCurrentQuery()
84-
const { refetch, ...rest } = unwrap(result)
85-
const unwrappedResult = {
86-
...rest,
87-
88-
// hydrate() expects a QueryState object, which is similar but not
89-
// quite the same as a QueryObserverResult object. Thus, for now, we're
90-
// copying over the missing properties from state in order to support hydration
91-
dataUpdateCount: query.state.dataUpdateCount,
92-
fetchFailureCount: query.state.fetchFailureCount,
93-
// Removing these properties since they might not be serializable
94-
// fetchFailureReason: query.state.fetchFailureReason,
95-
// fetchMeta: query.state.fetchMeta,
96-
isInvalidated: query.state.isInvalidated,
97-
}
130+
const unwrappedResult = hydrateableObserverResult(query, result)
98131

99132
if (unwrappedResult.isError) {
100-
if (process.env['NODE_ENV'] === 'development') {
101-
console.error(unwrappedResult.error)
102-
}
103133
reject(unwrappedResult.error)
104-
}
105-
if (unwrappedResult.isSuccess) {
106-
// Use of any here is fine
107-
// We cannot include refetch since it is not serializable
108-
resolve(unwrappedResult as any)
134+
} else {
135+
resolve(unwrappedResult)
109136
}
110137
})()
111138
})
@@ -148,7 +175,7 @@ export function createBaseQuery<
148175
let unsubscribe: (() => void) | null = null
149176

150177
const [queryResource, { refetch, mutate }] = createResource<
151-
QueryObserverResult<TData, TError> | undefined
178+
ResourceData | undefined
152179
>(
153180
() => {
154181
return new Promise((resolve, reject) => {
@@ -159,8 +186,10 @@ export function createBaseQuery<
159186
unsubscribe = createClientSubscriber()
160187
}
161188
}
189+
162190
if (!state.isLoading) {
163-
resolve(state)
191+
const query = observer.getCurrentQuery()
192+
resolve(hydrateableObserverResult(query, state))
164193
}
165194
})
166195
},

0 commit comments

Comments
 (0)