Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f459f28
commit ef98032
Showing
13 changed files
with
802 additions
and
416 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "@tanstack/query-example-tsr-suspense-streaming", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "node server", | ||
"build": "npm run build:client && npm run build:server", | ||
"build:client": "vite build src/entry-client.tsx --outDir dist/client", | ||
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", | ||
"serve": "NODE_ENV=production node server" | ||
}, | ||
"dependencies": { | ||
"@tanstack/react-router": "0.0.1-beta.248", | ||
"@tanstack/react-router-server": "0.0.1-beta.248", | ||
"@tanstack/router-devtools": "0.0.1-beta.248", | ||
"@tanstack/react-query-next-experimental": "workspace:*", | ||
"@tanstack/react-query": "workspace:*", | ||
"@tanstack/react-query-devtools": "workspace:*", | ||
"compression": "^1.7.4", | ||
"express": "^4.18.2", | ||
"get-port": "^7.0.0", | ||
"isbot": "^3.7.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/express": "^4.17.14", | ||
"@vitejs/plugin-react": "^4", | ||
"vite": "^4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import express, { Router } from 'express' | ||
import getPort, { portNumbers } from 'get-port' | ||
|
||
const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD | ||
|
||
const api = Router().get('/wait', async (req, res) => { | ||
const wait = +req.query.wait || 0 | ||
await new Promise((resolve) => setTimeout(resolve, wait)) | ||
res.json(`waited ${wait}ms`) | ||
}) | ||
|
||
export async function createServer( | ||
root = process.cwd(), | ||
isProd = process.env.NODE_ENV === 'production', | ||
hmrPort, | ||
) { | ||
const app = express() | ||
|
||
app.use('/api', api) | ||
|
||
/** | ||
* @type {import('vite').ViteDevServer} | ||
*/ | ||
let vite | ||
if (!isProd) { | ||
vite = await ( | ||
await import('vite') | ||
).createServer({ | ||
root, | ||
logLevel: isTest ? 'error' : 'info', | ||
server: { | ||
middlewareMode: true, | ||
watch: { | ||
// During tests we edit the files too fast and sometimes chokidar | ||
// misses change events, so enforce polling for consistency | ||
usePolling: true, | ||
interval: 100, | ||
}, | ||
hmr: { | ||
port: hmrPort, | ||
}, | ||
}, | ||
appType: 'custom', | ||
}) | ||
// use vite's connect instance as middleware | ||
app.use(vite.middlewares) | ||
} else { | ||
app.use((await import('compression')).default()) | ||
} | ||
|
||
app.use('*', async (req, res, next) => { | ||
if (req.url.startsWith('/api')) return next() | ||
|
||
try { | ||
const url = req.originalUrl | ||
|
||
if (url.includes('.')) { | ||
console.warn(`${url} is not valid router path`) | ||
res.status(404) | ||
res.end(`${url} is not valid router path`) | ||
return | ||
} | ||
|
||
// Extract the head from vite's index transformation hook | ||
let viteHead = !isProd | ||
? await vite.transformIndexHtml( | ||
url, | ||
`<html><head></head><body></body></html>`, | ||
) | ||
: '' | ||
|
||
viteHead = viteHead.substring( | ||
viteHead.indexOf('<head>') + 6, | ||
viteHead.indexOf('</head>'), | ||
) | ||
|
||
const entry = await (async () => { | ||
if (!isProd) { | ||
return vite.ssrLoadModule('/src/entry-server.tsx') | ||
} else { | ||
return import('./dist/server/entry-server.tsx') | ||
} | ||
})() | ||
|
||
console.log('Rendering: ', url, '...') | ||
entry.render({ req, res, url, head: viteHead }) | ||
} catch (e) { | ||
!isProd && vite.ssrFixStacktrace(e) | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) | ||
} | ||
}) | ||
|
||
return { app, vite } | ||
} | ||
|
||
if (!isTest) { | ||
createServer().then(async ({ app }) => | ||
app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => { | ||
console.log('Client Server: http://localhost:3000') | ||
}), | ||
) | ||
} |
10 changes: 10 additions & 0 deletions
10
examples/react/tsr-suspense-streaming/src/entry-client.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import * as React from 'react' | ||
import ReactDOM from 'react-dom/client' | ||
|
||
import { StartClient } from '@tanstack/react-router-server/client' | ||
import { createRouter } from './router' | ||
|
||
const router = createRouter() | ||
router.hydrate() | ||
|
||
ReactDOM.hydrateRoot(document, <StartClient router={router} />) |
78 changes: 78 additions & 0 deletions
78
examples/react/tsr-suspense-streaming/src/entry-server.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import * as React from 'react' | ||
import ReactDOMServer, { PipeableStream } from 'react-dom/server' | ||
import { createMemoryHistory } from '@tanstack/react-router' | ||
import { | ||
StartServer, | ||
transformStreamWithRouter, | ||
} from '@tanstack/react-router-server/server' | ||
import isbot from 'isbot' | ||
import { ServerResponse } from 'http' | ||
import express from 'express' | ||
|
||
// index.js | ||
import { createRouter } from './router' | ||
import { QueryClient } from '@tanstack/react-query' | ||
|
||
type ReactReadableStream = ReadableStream<Uint8Array> & { | ||
allReady?: Promise<void> | undefined | ||
} | ||
|
||
export async function render(opts: { | ||
url: string | ||
head: string | ||
req: express.Request | ||
res: ServerResponse | ||
}) { | ||
const router = createRouter() | ||
|
||
const memoryHistory = createMemoryHistory({ | ||
initialEntries: [opts.url], | ||
}) | ||
|
||
// Update the history and context | ||
router.update({ | ||
history: memoryHistory, | ||
}) | ||
|
||
// Wait for the router to load critical data | ||
// (streamed data will continue to load in the background) | ||
await router.load() | ||
|
||
// Track errors | ||
let didError = false | ||
|
||
// Clever way to get the right callback. Thanks Remix! | ||
const callbackName = isbot(opts.req.headers['user-agent']) | ||
? 'onAllReady' | ||
: 'onShellReady' | ||
|
||
// Render the app to a readable stream | ||
let stream!: PipeableStream | ||
|
||
await new Promise<void>((resolve) => { | ||
stream = ReactDOMServer.renderToPipeableStream( | ||
<StartServer router={router} />, | ||
{ | ||
[callbackName]: () => { | ||
opts.res.statusCode = didError ? 500 : 200 | ||
opts.res.setHeader('Content-Type', 'text/html') | ||
resolve() | ||
}, | ||
onError: (err) => { | ||
didError = true | ||
console.log(err) | ||
}, | ||
}, | ||
) | ||
}) | ||
|
||
// Add our Router transform to the stream | ||
const transforms = [transformStreamWithRouter(router)] | ||
|
||
const transformedStream = transforms.reduce( | ||
(stream, transform) => stream.pipe(transform as any), | ||
stream, | ||
) | ||
|
||
transformedStream.pipe(opts.res) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' | ||
|
||
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' | ||
import { Outlet, useRouter } from '@tanstack/react-router' | ||
import { useState } from 'react' | ||
|
||
function useInjectHTML(cb: () => string) { | ||
const router = useRouter() | ||
router.injectHtml(cb) | ||
} | ||
|
||
export function Providers() { | ||
const [queryClient] = useState( | ||
() => | ||
new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
staleTime: 5 * 1000, | ||
}, | ||
}, | ||
}), | ||
) | ||
|
||
return ( | ||
<QueryClientProvider client={queryClient}> | ||
<ReactQueryStreamedHydration useInjectServerHTML={useInjectHTML as any}> | ||
<Outlet /> | ||
</ReactQueryStreamedHydration> | ||
{<ReactQueryDevtools initialIsOpen={false} />} | ||
</QueryClientProvider> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { useSuspenseQuery } from '@tanstack/react-query' | ||
import { Route, Router, rootRouteWithContext } from '@tanstack/react-router' | ||
import { Suspense } from 'react' | ||
|
||
import { Providers } from './providers' | ||
|
||
function WaitComponent(props: { wait: number }) { | ||
const { data } = useSuspenseQuery({ | ||
queryKey: ['wait', props.wait], | ||
queryFn: async () => { | ||
const path = `/api/wait?wait=${props.wait}` | ||
const url = 'http://localhost:3000' + path | ||
|
||
console.log('fetching', url) | ||
const res: string = await ( | ||
await fetch(url, { | ||
cache: 'no-store', | ||
}) | ||
).json() | ||
return res | ||
}, | ||
}) | ||
|
||
return <div>result: {data}</div> | ||
} | ||
|
||
const rootRoute = rootRouteWithContext()({ | ||
component: RootComponent, | ||
}) | ||
|
||
function RootComponent() { | ||
return ( | ||
<div> | ||
<h1>TSR + TSQ Automatic Streaming Hydration</h1> | ||
<Suspense fallback={'Global suspense boundary yeyo'}> | ||
<Providers /> | ||
</Suspense> | ||
</div> | ||
) | ||
} | ||
|
||
const indexRoute = new Route({ | ||
getParentRoute: () => rootRoute, | ||
path: '/', | ||
component: IndexRouteComponent, | ||
}) | ||
function IndexRouteComponent() { | ||
return ( | ||
<> | ||
<Suspense fallback={<div>waiting 100....</div>}> | ||
<WaitComponent wait={100} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 200....</div>}> | ||
<WaitComponent wait={200} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 300....</div>}> | ||
<WaitComponent wait={300} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 400....</div>}> | ||
<WaitComponent wait={400} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 500....</div>}> | ||
<WaitComponent wait={500} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 600....</div>}> | ||
<WaitComponent wait={600} /> | ||
</Suspense> | ||
<Suspense fallback={<div>waiting 700....</div>}> | ||
<WaitComponent wait={700} /> | ||
</Suspense> | ||
|
||
<fieldset> | ||
<legend> | ||
combined <code>Suspense</code>-container | ||
</legend> | ||
<Suspense | ||
fallback={ | ||
<> | ||
<div>waiting 800....</div> | ||
<div>waiting 900....</div> | ||
<div>waiting 1000....</div> | ||
</> | ||
} | ||
> | ||
<WaitComponent wait={800} /> | ||
<WaitComponent wait={900} /> | ||
<WaitComponent wait={1000} /> | ||
</Suspense> | ||
</fieldset> | ||
</> | ||
) | ||
} | ||
|
||
const routeTree = rootRoute.addChildren([indexRoute]) | ||
|
||
export function createRouter() { | ||
return new Router({ | ||
routeTree, | ||
}) | ||
} | ||
|
||
declare module '@tanstack/react-router' { | ||
interface Register { | ||
router: ReturnType<typeof createRouter> | ||
} | ||
} |
Oops, something went wrong.