Skip to content

Commit e87d483

Browse files
Marc MacLeodardeoraTkDodo
authored
feat(solid-query): SSR streaming support (#4840)
* feat(solid-query): `createQuery` SSR support * fix(solid-query):Bug Fixes & Streaming SSR Support Changed query checkers to use `isInitialLoading` Fix hydration key bug for streaming SSR Add `deferStream` option for streaming SSR * fix(solid-query): Infinite query types * docs(solid-query): re-work ssr example * fix(solid-query): improved async + stream support * refactor(solid-query): entire state in resource Store the entire query state in the resource, so that we can leverage native resource hydration to hydrate the query on the client as it’s streamed in. * passing tests * pnpm install * Add example of generatlized query boundary … just one possible approach * fix(solid-query): do not refetch if hydrated Even if staleTime is not set * add wrapping div back to example * this setState is not necessary * fix(solid-query): SSR fixes --------- Co-authored-by: Aryan Deora <[email protected]> Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 4076eff commit e87d483

33 files changed

+3194
-504
lines changed

examples/solid/basic-graphql-request/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@tanstack/solid-query": "^4.3.9",
1414
"graphql": "^16.6.0",
1515
"graphql-request": "^5.0.0",
16-
"solid-js": "^1.5.1"
16+
"solid-js": "^1.6.2"
1717
},
1818
"devDependencies": {
1919
"typescript": "4.7.4",

examples/solid/basic-typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"license": "MIT",
1212
"dependencies": {
1313
"@tanstack/solid-query": "^4.3.9",
14-
"solid-js": "^1.5.1"
14+
"solid-js": "^1.6.2"
1515
},
1616
"devDependencies": {
1717
"typescript": "4.7.4",

examples/solid/default-query-function/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"license": "MIT",
1212
"dependencies": {
1313
"@tanstack/solid-query": "^4.3.9",
14-
"solid-js": "^1.5.1"
14+
"solid-js": "^1.6.2"
1515
},
1616
"devDependencies": {
1717
"typescript": "4.7.4",

examples/solid/simple/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"license": "MIT",
1212
"dependencies": {
1313
"@tanstack/solid-query": "^4.3.9",
14-
"solid-js": "^1.5.1"
14+
"solid-js": "^1.6.2"
1515
},
1616
"devDependencies": {
1717
"@tanstack/eslint-plugin-query": "^4.13.0",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": ["../../../.eslintrc"],
3+
"rules": {
4+
"react/react-in-jsx-scope": "off",
5+
"import/no-unresolved": "off"
6+
}
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
dist
3+
.solid
4+
.output
5+
.vercel
6+
.netlify
7+
netlify
8+
9+
# dependencies
10+
/node_modules
11+
12+
# IDEs and editors
13+
/.idea
14+
.project
15+
.classpath
16+
*.launch
17+
.settings/
18+
19+
# Temp
20+
gitignore
21+
22+
# System Files
23+
.DS_Store
24+
Thumbs.db
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
To run this example:
2+
3+
- `npm install`
4+
- `npm run start`
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "solid-start-streaming",
3+
"scripts": {
4+
"dev": "solid-start dev",
5+
"build": "solid-start build",
6+
"start": "solid-start start"
7+
},
8+
"type": "module",
9+
"devDependencies": {
10+
"@types/node": "^18.11.9",
11+
"esbuild": "^0.14.54",
12+
"postcss": "^8.4.18",
13+
"solid-start-node": "^0.2.0",
14+
"typescript": "^4.8.4",
15+
"vite": "^3.1.8"
16+
},
17+
"dependencies": {
18+
"@tanstack/solid-query": "^4.3.9",
19+
"@solidjs/meta": "^0.28.2",
20+
"@solidjs/router": "^0.6.0",
21+
"solid-js": "^1.6.9",
22+
"solid-start": "^0.2.15",
23+
"undici": "^5.11.0"
24+
},
25+
"engines": {
26+
"node": ">=16.8"
27+
}
28+
}
Binary file not shown.
Loading
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ParentComponent } from 'solid-js'
2+
3+
export interface ExampleProps {
4+
title: string
5+
deferStream?: boolean
6+
sleep?: number
7+
}
8+
9+
export const Example: ParentComponent<ExampleProps> = (props) => {
10+
return (
11+
<div class="example">
12+
<div class="example__header">
13+
<div class="example__title">{props.title}</div>
14+
<div>[deferStream={String(props.deferStream || false)}]</div>
15+
<div style={{ 'margin-left': '10px' }}>
16+
[simulated sleep: {props.sleep || 0}ms]
17+
</div>
18+
</div>
19+
20+
{props.children}
21+
</div>
22+
)
23+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { createQuery } from '@tanstack/solid-query'
2+
import type { Component } from 'solid-js'
3+
import { resetErrorBoundaries } from 'solid-js'
4+
import { createSignal } from 'solid-js'
5+
import { For } from 'solid-js'
6+
import { fetchPost } from '~/utils/api'
7+
import { Example } from './example'
8+
import { QueryBoundary } from './query-boundary'
9+
10+
export interface PostViewerProps {
11+
deferStream?: boolean
12+
sleep?: number
13+
simulateError?: boolean
14+
}
15+
16+
export const PostViewer: Component<PostViewerProps> = (props) => {
17+
const [simulateError, setSimulateError] = createSignal(props.simulateError)
18+
const [postId, setPostId] = createSignal(1)
19+
20+
const query = createQuery(() => ({
21+
queryKey: ['posts', postId()],
22+
queryFn: () =>
23+
fetchPost({
24+
postId: postId(),
25+
sleep: props.sleep,
26+
simulateError:
27+
simulateError() || (simulateError() !== false && postId() === 5),
28+
}),
29+
deferStream: props.deferStream,
30+
}))
31+
32+
return (
33+
<Example
34+
title="Post Query"
35+
deferStream={props.deferStream}
36+
sleep={props.sleep}
37+
>
38+
<div style={{ 'margin-top': '20px' }}>
39+
<button
40+
onClick={() => {
41+
setPostId(Math.max(postId() - 1, 1))
42+
resetErrorBoundaries()
43+
}}
44+
>
45+
Previous Post
46+
</button>
47+
<button
48+
onClick={() => {
49+
setPostId(Math.min(postId() + 1, 100))
50+
resetErrorBoundaries()
51+
}}
52+
>
53+
Next Post
54+
</button>
55+
</div>
56+
57+
{/* NOTE: without this extra wrapping div, for some reason solid ends up printing two errors... feels like a bug in solid. */}
58+
<div>
59+
<QueryBoundary
60+
query={query}
61+
loadingFallback={<div class="loader">loading post...</div>}
62+
errorFallback={
63+
<div>
64+
<div class="error">{query.error?.message}</div>
65+
<button
66+
onClick={() => {
67+
setSimulateError(false)
68+
query.refetch()
69+
}}
70+
>
71+
retry
72+
</button>
73+
</div>
74+
}
75+
>
76+
{(posts) => (
77+
<For each={posts}>
78+
{(post) => (
79+
<div style={{ 'margin-top': '20px' }}>
80+
<b>
81+
[post {postId()}] {post.title}
82+
</b>
83+
<p>{post.body}</p>
84+
</div>
85+
)}
86+
</For>
87+
)}
88+
</QueryBoundary>
89+
</div>
90+
</Example>
91+
)
92+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
3+
import type { CreateQueryResult } from '@tanstack/solid-query'
4+
import type { JSX } from 'solid-js'
5+
import { Match, Suspense, Switch } from 'solid-js'
6+
7+
export interface QueryBoundaryProps<T = unknown> {
8+
query: CreateQueryResult<T, Error>
9+
10+
/**
11+
* Triggered when the data is initially loading.
12+
*/
13+
loadingFallback?: JSX.Element
14+
15+
/**
16+
* Triggered when fetching is complete, but the returned data was falsey.
17+
*/
18+
notFoundFallback?: JSX.Element
19+
20+
/**
21+
* Triggered when the query results in an error.
22+
*/
23+
errorFallback?: JSX.Element
24+
25+
/**
26+
* Triggered when fetching is complete, and the returned data is not falsey.
27+
*/
28+
children: (data: Exclude<T, null | false | undefined>) => JSX.Element
29+
}
30+
31+
/**
32+
* Convenience wrapper that handles suspense and errors for queries. Makes the results of query.data available to
33+
* children (as a render prop) in a type-safe way.
34+
*/
35+
export function QueryBoundary<T>(props: QueryBoundaryProps<T>) {
36+
return (
37+
<Suspense fallback={props.loadingFallback}>
38+
<Switch>
39+
<Match when={props.query.isError}>
40+
{props.errorFallback ? (
41+
props.errorFallback
42+
) : (
43+
<div>
44+
<div class="error">{props.query.error?.message}</div>
45+
<button
46+
onClick={() => {
47+
props.query.refetch()
48+
}}
49+
>
50+
retry
51+
</button>
52+
</div>
53+
)}
54+
</Match>
55+
56+
<Match when={!props.query.isFetching && !props.query.data}>
57+
{props.notFoundFallback ? (
58+
props.notFoundFallback
59+
) : (
60+
<div>not found</div>
61+
)}
62+
</Match>
63+
64+
<Match when={props.query.data}>
65+
{props.children(
66+
props.query.data as Exclude<T, null | false | undefined>,
67+
)}
68+
</Match>
69+
</Switch>
70+
</Suspense>
71+
)
72+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createQuery } from '@tanstack/solid-query'
2+
import type { Component } from 'solid-js'
3+
import { createSignal } from 'solid-js'
4+
import { fetchUser } from '~/utils/api'
5+
import { Example } from './example'
6+
import { QueryBoundary } from './query-boundary'
7+
8+
export interface UserInfoProps {
9+
deferStream?: boolean
10+
sleep?: number
11+
simulateError?: boolean
12+
}
13+
14+
export const UserInfo: Component<UserInfoProps> = (props) => {
15+
const [simulateError, setSimulateError] = createSignal(props.simulateError)
16+
17+
const query = createQuery(() => ({
18+
queryKey: ['user'],
19+
queryFn: () =>
20+
fetchUser({ sleep: props.sleep, simulateError: simulateError() }),
21+
deferStream: props.deferStream,
22+
}))
23+
24+
return (
25+
<Example
26+
title="User Query"
27+
deferStream={props.deferStream}
28+
sleep={props.sleep}
29+
>
30+
<QueryBoundary
31+
query={query}
32+
loadingFallback={<div class="loader">loading user...</div>}
33+
errorFallback={
34+
<div>
35+
<div class="error">{query.error?.message}</div>
36+
<button
37+
onClick={() => {
38+
setSimulateError(false)
39+
query.refetch()
40+
}}
41+
>
42+
retry
43+
</button>
44+
</div>
45+
}
46+
>
47+
{(user) => (
48+
<>
49+
<div>id: {user.id}</div>
50+
<div>name: {user.name}</div>
51+
<div>queryTime: {user.queryTime}</div>
52+
</>
53+
)}
54+
</QueryBoundary>
55+
</Example>
56+
)
57+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { mount, StartClient } from 'solid-start/entry-client'
2+
3+
mount(() => <StartClient />, document)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {
2+
createHandler,
3+
renderStream,
4+
StartServer,
5+
} from 'solid-start/entry-server'
6+
7+
export default createHandler(
8+
renderStream((event) => {
9+
return <StartServer event={event} />
10+
}),
11+
)

0 commit comments

Comments
 (0)