Skip to content

Commit

Permalink
fix: add react-router-with-query experimental package
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jul 1, 2024
1 parent bf52d5a commit 23614fa
Show file tree
Hide file tree
Showing 21 changed files with 455 additions and 132 deletions.
1 change: 1 addition & 0 deletions docs/framework/react/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ const router = createRouter({
- Type: `RouterTransformer`
- Optional
- The transformer that will be used when sending data between the server and the client during SSR.
- Defaults to a very lightweight transformer that supports a few basic types. See the [SSR guide](../../guides/ssr) for more information.

#### `transformer.stringify` method

Expand Down
13 changes: 12 additions & 1 deletion docs/framework/react/guide/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,18 @@ Streaming dehydration/hydration is an advanced pattern that goes beyond markup a

## Data Transformers

When using SSR, data passed between the server and the client must be serialized before it is sent accross network-boundaries. By default, TanStack Router will serialize data using the default `JSON.parse` and `JSON.stringify` implementations. This, however, can lead to incorrect type-definitions when using objects such as `Date`/`Map`/`Set` etc. The Data Transformer API allows the usage of a custom serializer that can allow us to transparently use these data types when communicating across the network.
When using SSR, data passed between the server and the client must be serialized before it is sent accross network-boundaries. By default, TanStack Router will serialize data using a very lightweight serializer that supports a few basic types beyond JSON.stringify/JSON.parse.

Out of the box, the following types are supported:

- `Date`
- `undefined`

If you feel that there are other types that should be supported by default, please open an issue on the TanStack Router repository.

If you are using more complex data types like `Map`, `Set`, `BigInt`, etc, you may need to use a custom serializer to ensure that your type-definitions are accurate and your data is correctly serialized and deserialized. This is where the `transformer` option on `createRouter` comes in.

The Data Transformer API allows the usage of a custom serializer that can allow us to transparently use these data types when communicating across the network.

The following example shows usage with [SuperJSON](https://github.com/blitz-js/superjson), however, anything that implements [`Router Transformer`](../../api/router/RouterOptionsType#transformer-property) can be used.

Expand Down
109 changes: 12 additions & 97 deletions examples/react/start-basic-react-query/app/router.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,26 @@
import {
QueryClient,
QueryClientProvider,
dehydrate,
hashKey,
hydrate,
} from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
import { NotFound } from './components/NotFound'
import type {
QueryObserverResult,
UseQueryOptions,
} from '@tanstack/react-query'

// NOTE: Most of the integration code found here is experimental and will
// definitely end up in a more streamlined API in the future. This is just
// to show what's possible with the current APIs.

export function createRouter() {
const seenQueryKeys = new Set<string>()

const queryClient = new QueryClient({
defaultOptions: {
queries: {
_experimental_beforeQuery: (options: UseQueryOptions) => {
// On the server, check if we've already seen the query before
if (router.isServer) {
if (seenQueryKeys.has(hashKey(options.queryKey))) {
return
}

seenQueryKeys.add(hashKey(options.queryKey))

// If we haven't seen the query and we have data for it,
// That means it's going to get dehydrated with critical
// data, so we can skip the injection
if (queryClient.getQueryData(options.queryKey) !== undefined) {
;(options as any).__skipInjection = true
return
}
} else {
// On the client, pick up the deferred data from the stream
const dehydratedClient = router.getStreamedValue<any>(
'__QueryClient__' + hashKey(options.queryKey),
)

// If we have data, hydrate it into the query client
if (dehydratedClient && !dehydratedClient.hydrated) {
dehydratedClient.hydrated = true
hydrate(queryClient, dehydratedClient)
}
}
},
_experimental_afterQuery: (
options: UseQueryOptions,
_result: QueryObserverResult,
) => {
// On the server (if we're not skipping injection)
// send down the dehydrated query
if (
router.isServer &&
!(options as any).__skipInjection &&
queryClient.getQueryData(options.queryKey) !== undefined
) {
router.streamValue(
'__QueryClient__' + hashKey(options.queryKey),
dehydrate(queryClient, {
shouldDehydrateMutation: () => false,
shouldDehydrateQuery: (query) =>
hashKey(query.queryKey) === hashKey(options.queryKey),
}),
)
}
},
} as any,
},
})

const router = createTanStackRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
dehydrate: () => ({
// When critical data is dehydrated, we also dehydrate the query client
dehydratedQueryClient: dehydrate(queryClient),
const queryClient = new QueryClient()

return routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
}),
hydrate: ({ dehydratedQueryClient }) => {
// On the client, hydrate the query client with the dehydrated data
hydrate(queryClient, dehydratedQueryClient)
},
context: {
// Pass the query client to the context, so we can access it in loaders
queryClient,
},
// Wrap the app in a QueryClientProvider
Wrap: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})

return router
queryClient,
)
}

declare module '@tanstack/react-router' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const deferredQueryOptions = () =>
return {
message: `Hello deferred from the server!`,
status: 'success',
time: new Date(),
}
},
})
Expand Down Expand Up @@ -46,6 +47,7 @@ function DeferredQuery() {
<h1>Deferred Query</h1>
<div>Status: {deferredQuery.data.status}</div>
<div>Message: {deferredQuery.data.message}</div>
<div>Time: {deferredQuery.data.time.toISOString()}</div>
</div>
)
}
1 change: 1 addition & 0 deletions examples/react/start-basic-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@tanstack/react-query": "^5.49.2",
"@tanstack/react-query-devtools": "^5.48.0",
"@tanstack/react-router": "^1.43.3",
"@tanstack/react-router-with-query": "^1.43.2",
"@tanstack/router-devtools": "^1.43.3",
"@tanstack/router-vite-plugin": "^1.43.1",
"@tanstack/start": "^1.43.3",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"type": "module",
"scripts": {
"clean": "pnpm --filter \"./packages/**\" run clean",
"preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...')} else {process.exit(1)}\" || npx -y only-allow pnpm",
"preinstall": "node -e \"if(process.env.CI == 'true') {console.info('Skipping preinstall...')} else {process.exit(1)}\" || npx -y only-allow pnpm",
"test": "pnpm run test:ci",
"test:pr": "nx affected --targets=test:format,test:eslint,test:unit,test:build,build",
"test:ci": "nx run-many --targets=test:eslint,test:unit,test:types,test:build,build",
Expand Down Expand Up @@ -72,6 +72,7 @@
"@tanstack/router-generator": "workspace:*",
"@tanstack/router-plugin": "workspace:*",
"@tanstack/router-vite-plugin": "workspace:*",
"@tanstack/react-router-with-query": "workspace:*",
"@tanstack/start": "workspace:*",
"@tanstack/start-vite-plugin": "workspace:*",
"temp-react": "0.0.0-experimental-035a41c4e-20230704",
Expand Down
31 changes: 31 additions & 0 deletions packages/react-router-with-query/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<img src="https://static.scarf.sh/a.png?x-pxid=d988eb79-b0fc-4a2b-8514-6a1ab932d188" />

# TanStack React Router

![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png)

🤖 Type-safe router w/ built-in caching & URL state management for React!

<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
</a><a href="https://npmjs.com/package/@tanstack/react-router" target="\_parent">
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/router.svg" />
</a><a href="https://bundlephobia.com/result?p=@tanstack/react-router" target="\_parent">
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-router" />
</a><a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a><a href="https://github.com/tanstack/router/discussions">
<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
</a><a href="https://bestofjs.org/projects/router"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=tanstack%2Frouter%26since=daily" /></a><a href="https://github.com/tanstack/router" target="\_parent">
<img alt="" src="https://img.shields.io/github/stars/tanstack/router.svg?style=social&label=Star" />
</a><a href="https://twitter.com/tan_stack" target="\_parent">
<img alt="" src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social&label=Follow @TanStack" />
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow @TannerLinsley" />
</a>

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/tannerlinsley/react-query), [React Table](https://github.com/tanstack/react-table), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)

## Visit [tanstack.com/router](https://tanstack.com/router) for docs, guides, API and more!
31 changes: 31 additions & 0 deletions packages/react-router-with-query/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check

import pluginReact from '@eslint-react/eslint-plugin'
import pluginReactHooks from 'eslint-plugin-react-hooks'
import rootConfig from '../../eslint.config.js'

export default [
...rootConfig,
{
files: ['**/*.{ts,tsx}'],
...pluginReact.configs.recommended,
},
{
plugins: {
'react-hooks': pluginReactHooks,
},
rules: {
'@eslint-react/no-unstable-context-value': 'off',
'@eslint-react/no-unstable-default-props': 'off',
'@eslint-react/dom/no-missing-button-type': 'off',
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
},
},
{
files: ['**/__tests__/**'],
rules: {
'ts/no-unnecessary-condition': 'off',
},
},
]
73 changes: 73 additions & 0 deletions packages/react-router-with-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@tanstack/react-router-with-query",
"version": "1.43.2",
"description": "",
"author": "Tanner Linsley",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/TanStack/router.git",
"directory": "packages/react-router"
},
"homepage": "https://tanstack.com/router",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"types": "dist/esm/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "tsc --noEmit",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch",
"test:build": "publint --strict",
"build": "vite build"
},
"keywords": [
"react",
"location",
"router",
"routing",
"async",
"async router",
"typescript"
],
"engines": {
"node": ">=12"
},
"files": [
"dist",
"src"
],
"dependencies": {},
"devDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@tanstack/react-router": "workspace:*",
"@tanstack/react-query": ">=5.49.2"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@tanstack/react-router": ">=1.43.2",
"@tanstack/react-query": ">=5.49.2"
}
}
Loading

0 comments on commit 23614fa

Please sign in to comment.