Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-query): minimal integrate with react query by using queryOptions #46

Merged
merged 18 commits into from
Dec 11, 2024
172 changes: 172 additions & 0 deletions apps/content/content/docs/client/react-query.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
title: React Query
description: Simplify React Query usage with minimal integration using ORPC and TanStack Query
---

## Installation

```package-install
@orpc/client @orpc/react-query @tanstack/react-query
```

## Setup

### Using a Global Client

```ts twoslash
import { createORPCReactQueryUtils } from '@orpc/react-query';
import { createORPCClient } from '@orpc/client';
import type { router } from 'examples/server';

// Create an ORPC client
export const client = createORPCClient<typeof router>({
baseURL: 'http://localhost:3000/api',
});

// Create React Query utilities for ORPC
export const orpc = createORPCReactQueryUtils(client);

// @noErrors
orpc.getting.
// ^|
```

### Using Context with a Client

```tsx twoslash
import { createORPCReactQueryUtils, RouterUtils } from '@orpc/react-query';
import { createORPCClient } from '@orpc/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { router } from 'examples/server';
import * as React from 'react';

const ORPCContext = React.createContext<RouterUtils<typeof router> | undefined>(undefined);

export function useORPC() {
const orpc = React.useContext(ORPCContext);

if (!orpc) {
throw new Error('ORPCContext is not available.');
}

return orpc;
}

export function ORPCProvider({ children }: { children: React.ReactNode }) {
const [client] = React.useState(() =>
createORPCClient<typeof router>({
baseURL: 'http://localhost:3000/api',
})
);
const [queryClient] = React.useState(() => new QueryClient());

const orpc = React.useMemo(() => createORPCReactQueryUtils(client), [client]);

return (
<QueryClientProvider client={queryClient}>
<ORPCContext.Provider value={orpc}>
{children}
</ORPCContext.Provider>
</QueryClientProvider>
);
}

// Example usage
const orpc = useORPC();
// @noErrors
orpc.getting.
// ^|
```

## Multiple ORPC Instances

To prevent conflicts when using multiple ORPC instances, you can provide a unique base path to `createORPCReactQueryUtils`.

```tsx twoslash
import { createORPCReactQueryUtils } from '@orpc/react-query'

// Create separate ORPC instances with unique base paths
const userORPC = createORPCReactQueryUtils('fake-client' as any, ['__user-api__'])
const postORPC = createORPCReactQueryUtils('fake-client' as any, ['__post-api__'])
```

This ensures that each instance manages its own set of query keys and avoids any conflicts.

## Usage

### Standard Queries and Mutations

```ts twoslash
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { orpc } from 'examples/react-query'

// Fetch data
const { data: gettingData } = useQuery(orpc.getting.queryOptions({ input: { name: 'unnoq' } }))

// Use suspense query
const { data: postData } = useSuspenseQuery(
orpc.post.find.queryOptions({ input: { id: 'example' } }),
)

// Perform mutations
const { mutate: postMutate } = useMutation(orpc.post.create.mutationOptions())

// Invalidate queries
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: orpc.key() }) // Invalidate all queries
queryClient.invalidateQueries({ queryKey: orpc.post.find.key({ input: { id: 'example' } }) }) // Specific queries
```

> **Note**: This library enhances [TanStack Query](https://tanstack.com/query/latest) by managing query keys and functions for you, providing a seamless developer experience.


## Infinite Queries

Infinite queries require a `cursor` in the input field for pagination.

```tsx twoslash
import { os } from '@orpc/server';
import { z } from 'zod';
import { createORPCReactQueryUtils } from '@orpc/react-query';
import { useInfiniteQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query';
import * as React from 'react';

const router = {
user: {
list: os
.input(z.object({ cursor: z.number(), limit: z.number() }))
.func((input) => ({
nextCursor: input.cursor + input.limit,
users: [], // Fetch your actual data here
})),
},
};

const orpc = createORPCReactQueryUtils<typeof router>('fake-client' as any);

export function MyComponent() {
const query = useInfiniteQuery(
orpc.user.list.infiniteOptions({
input: { limit: 10 },
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
);

const query2 = useSuspenseInfiniteQuery(
orpc.user.list.infiniteOptions({
input: { limit: 10 },
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
);

return (
<div>
{query.isLoading && 'Loading...'}
{query.isSuccess && <div>Data: {JSON.stringify(query.data)}</div>}
{query.isError && <div>Error: {query.error.message}</div>}
</div>
);
}
```
43 changes: 27 additions & 16 deletions apps/content/content/home/landing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,35 @@ Our [Vanilla Client](/docs/client/vanilla) is fully typed and doesn't rely on ge
## Seamless Integration with TanStack Query

```tsx
const utils = orpc.useUtils()
const { mutate, isPending } = orpc.getting.useMutation({
onSuccess(){
utils.invalidate() // invalidate all queries
}
})

mutate({
id: 1992n,
user: {
name: 'unnoq',
avatar: document.getElementById('avatar').files[0],
}
})
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Fetch data with oRPC
const { data, status } = useQuery(
orpc.post.find.queryOptions({ input: { id: 'example' } })
);

// Perform a mutation and invalidate related queries on success
const { mutate, isPending } = useMutation(
orpc.getting.mutationOptions({
onSuccess() {
queryClient.invalidateQueries({
queryKey: orpc.post.find.key({ input: { id: 'example' } }),
});
},
})
);

// Execute mutation with structured input
mutate({
id: 1992n,
user: {
name: 'unnoq',
avatar: document.getElementById('avatar').files[0], // Handle file uploads
},
});
```

oRPC's [TanStack Query integration](/docs/client/react) includes over 10 hooks and 29+ utilities,
such as `useQuery`, `useSuspenseQuery`, `useMutation`, `invalidate`, and more. Absolutely everything is typesafe!
Learn more about [React Query Integration](/docs/client/react-query).

## Access via OpenAPI Standard

Expand Down
4 changes: 4 additions & 0 deletions apps/content/examples/react-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { router } from 'examples/server'
import { createORPCReactQueryUtils } from '@orpc/react-query'

export const orpc = createORPCReactQueryUtils<typeof router /** or contract router */>('fake-client' as any)
1 change: 1 addition & 0 deletions apps/content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@orpc/next": "workspace:*",
"@orpc/openapi": "workspace:*",
"@orpc/react": "workspace:*",
"@orpc/react-query": "workspace:*",
"@orpc/server": "workspace:*",
"@orpc/zod": "workspace:*",
"@types/express": "^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions apps/content/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
{ "path": "../../packages/server" },
{ "path": "../../packages/client" },
{ "path": "../../packages/react" },
{ "path": "../../packages/react-query" },
{ "path": "../../packages/zod" },
{ "path": "../../packages/next" }
],
Expand Down
26 changes: 26 additions & 0 deletions packages/react-query/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Hidden folders and files
.*
!.gitignore
!.*.example

# Common generated folders
logs/
node_modules/
out/
dist/
dist-ssr/
build/
coverage/
temp/

# Common generated files
*.log
*.log.*
*.tsbuildinfo
*.vitest-temp.json
vite.config.ts.timestamp-*
vitest.config.ts.timestamp-*

# Common manual ignore files
*.local
*.pem
60 changes: 60 additions & 0 deletions packages/react-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "@orpc/react-query",
"type": "module",
"version": "0.0.0",
"license": "MIT",
"homepage": "https://orpc.unnoq.com",
"repository": {
"type": "git",
"url": "git+https://github.com/unnoq/orpc.git",
"directory": "packages/react-query"
},
"keywords": [
"unnoq",
"orpc",
"react-query",
"tanstack query",
"react"
],
"publishConfig": {
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./🔒/*": {
"types": "./dist/src/*.d.ts"
}
}
},
"exports": {
".": "./src/index.ts",
"./🔒/*": {
"types": "./src/*.ts"
}
},
"files": [
"!**/*.map",
"!**/*.tsbuildinfo",
"dist"
],
"scripts": {
"build": "tsup --clean --sourcemap --entry.index=src/index.ts --format=esm --onSuccess='tsc -b --noCheck'",
"build:watch": "pnpm run build --watch",
"type:check": "tsc -b"
},
"peerDependencies": {
"@orpc/client": "workspace:*",
"@orpc/contract": "workspace:*",
"@orpc/server": "workspace:*",
"@tanstack/react-query": ">=5.59.0",
"react": ">=18.3.0"
},
"dependencies": {
"@orpc/shared": "workspace:*"
},
"devDependencies": {
"zod": "^3.21.4"
}
}
9 changes: 9 additions & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRouterUtils } from './utils-router'

export * from './key'
export * from './types'
export * from './utils-general'
export * from './utils-procedure'
export * from './utils-router'

export const createORPCReactQueryUtils = createRouterUtils
17 changes: 17 additions & 0 deletions packages/react-query/src/key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { buildKey } from './key'

describe('buildKey', () => {
it('works', () => {
expect(buildKey(['path'])).toEqual(['__ORPC__', ['path'], {}])
expect(buildKey(['path'], { type: 'query' })).toEqual(['__ORPC__', ['path'], { type: 'query' }])

expect(buildKey(['path'], { type: 'query', input: { a: 1 } }))
.toEqual(['__ORPC__', ['path'], { type: 'query', input: { a: 1 } }])

expect(buildKey(['path'], { type: 'query', input: undefined }))
.toEqual(['__ORPC__', ['path'], { type: 'query' }])

expect(buildKey(['path'], { type: undefined, input: undefined }))
.toEqual(['__ORPC__', ['path'], { }])
})
})
27 changes: 27 additions & 0 deletions packages/react-query/src/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { PartialDeep } from '@orpc/shared'
import type { QueryKey } from '@tanstack/react-query'

export type KeyType = 'query' | 'infinite' | 'mutation' | undefined

export interface BuildKeyOptions<TType extends KeyType, TInput> {
type?: TType
input?: TType extends 'mutation' ? never : PartialDeep<TInput>
}

export function buildKey<TType extends KeyType, TInput>(
path: string[],
options?: BuildKeyOptions<TType, TInput>,
): QueryKey {
const withInput
= options?.input !== undefined ? { input: options?.input } : {}
const withType = options?.type !== undefined ? { type: options?.type } : {}

return [
'__ORPC__',
path,
{
...withInput,
...withType,
},
]
}
Loading
Loading