Skip to content

Commit

Permalink
fix: changes for SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jul 13, 2023
1 parent e43f26f commit b27f649
Show file tree
Hide file tree
Showing 53 changed files with 521 additions and 1,163 deletions.
6 changes: 3 additions & 3 deletions docs/guide/data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Data loading is a common concern for web applications and is extremely related t

You may be familiar with `getServerSideProps` from Next.js or or `loaders` from Remix/React-Router. Both of these APIs assumes that **the router will store and manage your data**. This approach is great for use cases covered by both of those libraries, but TanStack Router is designed to function a bit differently than you're used to. Let's dig in!

## TanStack Router **does not store your data**.
## TanStack Router **should not store your data**.

Most routers that support data fetching will store the data for you in memory on the client. This is fine, but puts a large responsibility and stress on the router to handle [many cross-cutting and complex challenges that come with managing server-data, client-side caches and mutations](https://tanstack.com/query/latest/docs/react/overview#motivation).
Most routers that support data fetching will store and manage the data for you as you navigate. This is fine, but puts a large responsibility and stress on the router to handle [many cross-cutting and complex challenges that come with managing server-data, client-side caches and mutations](https://tanstack.com/query/latest/docs/react/overview#motivation).

## TanStack Router **orchestrates your data fetching**.

Instead of storing your data, TanStack Router is designed to **coordinate** your data fetching. This means that you can use any data fetching library you want, and the router will coordinate the fetching of your data in a way that aligns with your users' navigation.
Instead of storing and managing your data, TanStack Router is designed to **coordinate** your data fetching. This means that you can use any data fetching library you want, and the router will coordinate the fetching of your data in a way that aligns with your users' navigation.

## What data fetching libraries are supported?

Expand Down
18 changes: 15 additions & 3 deletions docs/guide/route-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ const userRoute = new Route({
path: '$userId',
})

const routeConfig = rootRoute.addChildren([
usersRoute.addChildren([userRoute])
])
const routeConfig = rootRoute.addChildren([usersRoute.addChildren([userRoute])])
```

Dynamic segments can be accessed via the `params` object using the label you provided as the property key. For example, a path of `/users/$userId` would produce a `userId` param of `123` for the path `/users/123/details`:
Expand Down Expand Up @@ -179,4 +177,18 @@ In the above example, the `layout` route will not add or match any path in the U

> 🧠 An ID is required because every route must be uniquely identifiable, especially when using TypeScript so as to avoid type errors and accomplish autocomplete effectively.
## Identifying Routes via Search Params

Search Params by default are not used to identify matching paths mostly because they are extremely flexible, flat and can contain a lot of unrelated data to your actual route definition. However, in some cases you may choose to use them to uniquely identify a route match. For example, you may want to use a search param to identify a specific user in your application, you might model your url like this: `/user?userId=123`. This means that in your `user` route would need some extra help to identify a specific user. You can do this by adding a `getKey` function to your route:

```tsx
const userRoute = new Route({
getParentRoute: () => usersRoute,
path: 'user',
getKey: ({ search }) => search.userId,
})
```

---

Route paths are just the beginning of what you can do with route configuration. We'll explore more of those features later on.
4 changes: 2 additions & 2 deletions docs/guide/router-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const rootRoute = RootRoute({
component: () => {
const router = useRouter()

const breadcrumbs = router.state.currentMatches.map((match) => {
const breadcrumbs = router.state.matches.map((match) => {
const { routeContext } = match
return {
title: routeContext.getTitle(),
Expand All @@ -203,7 +203,7 @@ const rootRoute = RootRoute({
component: () => {
const router = useRouter()

const matchWithTitle = [...router.state.currentMatches]
const matchWithTitle = [...router.state.matches]
.reverse()
.find((d) => d.routeContext.getTitle)

Expand Down
7 changes: 2 additions & 5 deletions docs/guide/search-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,8 @@ const ProductList = () => {

You can access your route's validated search params anywhere in your app using:

- `router.state.currentLocation.state`
- `router.state.pendingLocation.state`
- `router.state.latestLocation.state`

Each one represent different states of the router. `currentLocation` is the current location of the router, `pendingLocation` is the location that the router is transitioning to, and `latestLocation` is most up-to-date representation of the location that the router has synced from the URL.
- `router.state.location.state` - The most up-to-date location of the router, regardless of loading state.
- `router.state.currentLocation.state` - The latest **loaded/resolved** location. This location is only set after a navigation has completed.

## Writing Search Params

Expand Down
1 change: 1 addition & 0 deletions examples/react/basic-ssr-streaming/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"dependencies": {
"@tanstack/react-loaders": "0.0.1-beta.105",
"@tanstack/react-start": "0.0.1-beta.96",
"@tanstack/router": "0.0.1-beta.104",
"@tanstack/router-devtools": "0.0.1-beta.104",
"@tanstack/router-cli": "0.0.1-beta.69",
Expand Down
20 changes: 5 additions & 15 deletions examples/react/basic-ssr-streaming/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import * as React from 'react'
import ReactDOM from 'react-dom/client'

import { App } from '.'
import { router } from './router'
import { loaderClient } from './loaderClient'
import { StartClient } from '@tanstack/react-start/client'
import { createRouter } from './router'

const { dehydratedRouter, dehydratedLoaderClient } = (window as any)
.__TSR_DEHYDRATED__
const router = createRouter()
router.hydrate()

// Hydrate the loader client first
loaderClient.hydrate(dehydratedLoaderClient)

// Hydrate the router next
router.hydrate(dehydratedRouter)

ReactDOM.hydrateRoot(
document,
<App router={router} loaderClient={loaderClient} head={''} />,
)
ReactDOM.hydrateRoot(document, <StartClient router={router} />)
79 changes: 20 additions & 59 deletions examples/react/basic-ssr-streaming/src/entry-server.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,39 @@
import * as React from 'react'
import ReactDOMServer from 'react-dom/server'
import { createMemoryHistory, Router, RouterProvider } from '@tanstack/router'
import { createMemoryHistory } from '@tanstack/router'
import { StartServer } from '@tanstack/react-start/server'
import isbot from 'isbot'
import jsesc from 'jsesc'
import { ServerResponse } from 'http'
import express from 'express'

// index.js
import '../src-old/fetch-polyfill'
import { createLoaderClient } from './loaderClient'
import { routeTree } from './routeTree'
import './fetch-polyfill'
import { createRouter } from './router'

async function getRouter(opts: { url: string }) {
const loaderClient = createLoaderClient()

const router = new Router({
routeTree: routeTree,
context: {
loaderClient,
},
})
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,
context: {
...router.context,
head: opts.head,
},
})

return { router, loaderClient }
}

export async function render(opts: {
url: string
head: string
req: express.Request
res: ServerResponse
}) {
const { router, loaderClient } = await getRouter(opts)

// ssrFooter: () => {
// // After the router has been fully loaded, serialize its
// // state right into the HTML. This way, the client can
// // hydrate the router with the same state that the server
// // used to render the HTML.
// const routerState = router.dehydrate()
// return (
// <>
// <script
// suppressHydrationWarning
// dangerouslySetInnerHTML={{
// __html: `
// window.__TANSTACK_DEHYDRATED_ROUTER__ = JSON.parse(${jsesc(
// JSON.stringify(routerState),
// {
// isScriptContext: true,
// wrap: true,
// json: true,
// },
// )})
// `,
// }}
// ></script>
// </>
// )
// },

// Kick off the router loading sequence, but don't wait for it to finish
router.load()
// 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
Expand All @@ -79,11 +44,7 @@ export async function render(opts: {
: 'onShellReady'

const stream = ReactDOMServer.renderToPipeableStream(
<RouterProvider
router={router}
loaderClient={loaderClient}
head={opts.head}
/>,
<StartServer router={router} />,
{
[callbackName]: () => {
opts.res.statusCode = didError ? 500 : 200
Expand Down
44 changes: 4 additions & 40 deletions examples/react/basic-ssr-streaming/src/loaderClient.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,13 @@
import { Loader, LoaderClient } from '@tanstack/react-loaders'

export type PostType = {
id: string
title: string
body: string
}

export const postsLoader = new Loader({
fn: async () => {
console.log('Fetching posts...')
await new Promise((r) =>
setTimeout(r, 300 + Math.round(Math.random() * 300)),
)
return fetch('https://jsonplaceholder.typicode.com/posts')
.then((d) => d.json() as Promise<PostType[]>)
.then((d) => d.slice(0, 10))
},
})

export const postLoader = new Loader({
fn: async (postId: string) => {
console.log(`Fetching post with id ${postId}...`)

await new Promise((r) => setTimeout(r, Math.round(Math.random() * 300)))

return fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(
(r) => r.json() as Promise<PostType>,
)
},
onInvalidate: async () => {
await postsLoader.invalidate()
},
})
import { LoaderClient } from '@tanstack/react-loaders'
import { postsLoader } from './routes/posts'
import { postLoader } from './routes/posts/$postId'

export const createLoaderClient = () => {
return new LoaderClient({
getLoaders: () => ({
postsLoader,
postLoader,
}),
getLoaders: () => ({ postsLoader, postLoader }),
})
}

export const loaderClient = createLoaderClient()

// Register things for typesafety
declare module '@tanstack/react-loaders' {
interface Register {
Expand Down
10 changes: 0 additions & 10 deletions examples/react/basic-ssr-streaming/src/routeTree.ts

This file was deleted.

60 changes: 48 additions & 12 deletions examples/react/basic-ssr-streaming/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
import { RegisteredLoaderClient } from '@tanstack/react-loaders'
import { Router } from '@tanstack/router'
import { loaderClient } from './loaderClient'
import { routeTree } from './routeTree'
import { LoaderClientProvider } from '@tanstack/react-loaders'

export interface RouterContext {
loaderClient: RegisteredLoaderClient
import { rootRoute } from './routes/root'
import { indexRoute } from './routes/index'
import { postsRoute } from './routes/posts'
import { postsIndexRoute } from './routes/posts/index'
import { postIdRoute } from './routes/posts/$postId'

import { createLoaderClient } from './loaderClient'
import React from 'react'

export type RouterContext = {
loaderClient: ReturnType<typeof createLoaderClient>
head: string
}

export const router = new Router({
routeTree,
context: {
loaderClient,
},
})
export const routeTree = rootRoute.addChildren([
indexRoute,
postsRoute.addChildren([postsIndexRoute, postIdRoute]),
])

export function createRouter() {
const loaderClient = createLoaderClient()

return new Router({
routeTree,
context: {
loaderClient,
head: '',
},
// On the server, dehydrate the loader client
dehydrate: () => {
return {
loaderClient: loaderClient.dehydrate(),
}
},
// On the client, rehydrate the loader client
hydrate: (dehydrated) => {
loaderClient.hydrate(dehydrated.loaderClient)
},
// Wrap our router in the loader client provider
Wrap: ({ children }) => {
return (
<LoaderClientProvider loaderClient={loaderClient}>
{children}
</LoaderClientProvider>
)
},
})
}

declare module '@tanstack/router' {
interface Register {
router: typeof router
router: ReturnType<typeof createRouter>
}
}
Loading

0 comments on commit b27f649

Please sign in to comment.