├── .github └── workflows │ ├── ci-cd.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.mjs ├── .prettierignore ├── .prettierrc.cjs ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── docs ├── .env.example ├── .gitignore ├── .npmrc ├── app.config.ts ├── app.vue ├── components │ ├── Footer.vue │ ├── Header.vue │ └── OgImage │ │ └── OgImageDocs.vue ├── content │ ├── 1.getting-started │ │ ├── 1.index.md │ │ ├── 2.usage.md │ │ └── _dir.yml │ ├── composables │ │ ├── 1.useQuery.md │ │ ├── 2.useMutation.md │ │ ├── 3.invalidate.md │ │ ├── 4.setQueryData.md │ │ ├── 5.key.md │ │ └── _dir.yml │ └── index.yml ├── error.vue ├── layouts │ └── docs.vue ├── nuxt.config.ts ├── nuxt.schema.ts ├── package.json ├── pages │ ├── [...slug].vue │ └── index.vue ├── pnpm-lock.yaml ├── public │ └── dna.svg ├── server │ ├── api │ │ └── search.json.get.ts │ └── tsconfig.json ├── tailwind.config.ts └── tsconfig.json ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ts └── types.ts ├── test ├── basic.test.ts ├── trpc │ ├── index.ts │ └── trpc.ts ├── tsconfig.json └── vue-app.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: 🚥 CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: '**' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | lint: 14 | name: 💅 Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: pnpm install 18 | uses: falcondev-it/.github/actions/pnpm-install@master 19 | 20 | - name: Cache ESLint & Prettier 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | .eslintcache 25 | node_modules/.cache/prettier/.prettier-cache 26 | key: eslint-prettier-cache-${{ runner.os }} 27 | 28 | - run: pnpm run lint:ci 29 | 30 | type-check: 31 | name: 🛃 Type Check 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: pnpm install 35 | uses: falcondev-it/.github/actions/pnpm-install@master 36 | 37 | - run: pnpm run type-check 38 | 39 | test: 40 | name: 🧪 Unit Tests 41 | needs: [type-check] 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: pnpm install 45 | uses: falcondev-it/.github/actions/pnpm-install@master 46 | 47 | - run: pnpm run test 48 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # eslint-disable no-irregular-whitespace, yaml/no-irregular-whitespace 2 | # prettier-ignore 3 | name:   4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | ref: 9 | description: "Force checkout a specific git ref (branch, tag, commit)" 10 | type: string 11 | 12 | concurrency: 13 | group: deploy 14 | cancel-in-progress: false # canceling could break state 15 | 16 | jobs: 17 | publish: 18 | name: Publish 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | ref: ${{ inputs.ref }} 25 | 26 | - uses: pnpm/action-setup@v3 27 | with: 28 | version: 10 29 | 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: 22.x 33 | cache: pnpm 34 | registry-url: https://registry.npmjs.org 35 | 36 | - run: pnpm install --frozen-lockfile 37 | 38 | - name: Build 39 | run: pnpm build 40 | 41 | - name: Publish 42 | run: pnpm publish --no-git-checks --tag $(npx simple-dist-tag) 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Release 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | description: Bump version 12 | type: choice 13 | required: true 14 | options: [⚙️ patch (v1.0.1), 🧩 minor (v1.1.0), ⭐️ major (v2.0.0)] 15 | push: 16 | tags: 17 | - 'v*' 18 | 19 | run-name: ${{ github.event_name == 'workflow_dispatch' && format('Bump {0}', startsWith(inputs.version, '⚙️') && 'patch ⚙️' || startsWith(inputs.version, '🧩') && 'minor 🧩' || 'major ⭐️') || github.ref_name }} 20 | 21 | env: 22 | ⚙️ patch (v1.0.1): patch 23 | 🧩 minor (v1.1.0): minor 24 | ⭐️ major (v2.0.0): major 25 | 26 | jobs: 27 | release: 28 | name: 🔖 Release 29 | runs-on: ubuntu-latest 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | outputs: 33 | RELEASE_REF: ${{ steps.bump-version.outputs.RELEASE_REF }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: lts/* 42 | 43 | - name: ⬆️ Bump version 44 | if: github.event_name == 'workflow_dispatch' 45 | id: bump-version 46 | run: | 47 | git config user.name "github-actions[bot]" 48 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 49 | 50 | npx bumpp ${{ env[inputs.version] }} -y -r --commit "chore(release): v%s" 51 | 52 | echo "RELEASE_REF=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" 53 | 54 | - name: 📢 Release 55 | run: npx changelogithub 56 | 57 | deploy: 58 | name: 🚀 Deploy 59 | needs: [release] 60 | uses: ./.github/workflows/deploy.yml 61 | secrets: inherit 62 | with: 63 | ref: ${{ needs.release.outputs.RELEASE_REF }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | dist/ 5 | .eslintcache 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | pnpm commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | CI=true pnpm lint-staged -------------------------------------------------------------------------------- /.lintstagedrc.mjs: -------------------------------------------------------------------------------- 1 | const runInPackage = undefined // replace with package name, eg: '@company/package' 2 | const pnpmExec = runInPackage ? `pnpm --filter ${runInPackage} exec ` : '' 3 | 4 | export default { 5 | '*.{vue,?([cm])[jt]s?(x),y?(a)ml,json?(c),md,html,?(s)css}': [ 6 | `${pnpmExec}eslint --fix --cache`, 7 | `${pnpmExec}prettier --write --cache`, 8 | ], 9 | '*.{vue,?([cm])ts?(x)}': () => `${pnpmExec}tsc -p tsconfig.json --noEmit --composite false`, // run once for all files 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .nuxt/ 3 | .output/ 4 | .temp/ 5 | node_modules/ 6 | 7 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('@louishaftmann/prettier-config') 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.organizeImports": "never" 8 | }, 9 | "eslint.experimental.useFlatConfig": true, 10 | "eslint.validate": [ 11 | "javascript", 12 | "javascriptreact", 13 | "typescript", 14 | "typescriptreact", 15 | "vue", 16 | "html", 17 | "markdown", 18 | "json", 19 | "jsonc", 20 | "yaml" 21 | ], 22 | "eslint.workingDirectories": [ 23 | { 24 | "mode": "auto" 25 | } 26 | ], 27 | "typescript.tsdk": "node_modules/typescript/lib" 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 falconDev IT GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tRPC Vue Query 2 | 3 | NPM version 4 | 5 | A tRPC wrapper around @tanstack/vue-query. This package provides a set of hooks to use tRPC with Vue Query. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pnpm add @falcondev-oss/trpc-vue-query 11 | ``` 12 | 13 | ## Documentation 14 | 15 | 👉 👈 16 | 17 | ## Usage with Vue 18 | 19 | ### 1. Create client & composable 20 | 21 | ```ts 22 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 23 | import { VueQueryPlugin, useQueryClient } from '@tanstack/vue-query' 24 | 25 | import type { AppRouter } from '../your_server/trpc' 26 | 27 | app.use(VueQueryPlugin) 28 | app.use({ 29 | install(app) { 30 | const queryClient = app.runWithContext(useQueryClient) 31 | const trpc = createTRPCVueQueryClient({ 32 | queryClient, 33 | trpc: { 34 | links: [ 35 | httpBatchLink({ 36 | url: '/api/trpc', 37 | }), 38 | ], 39 | }, 40 | }) 41 | 42 | app.provide('trpc', trpc) 43 | }, 44 | }) 45 | ``` 46 | 47 | ```ts 48 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 49 | 50 | import type { AppRouter } from '../your_server/trpc' 51 | 52 | export function useTRPC() { 53 | return inject('trpc') as ReturnType> 54 | } 55 | ``` 56 | 57 | ### 2. Use it in your components 58 | 59 | ```vue 60 | 63 | 64 | 69 | ``` 70 | 71 | ### 3. Passing vue-query options 72 | 73 | ```vue 74 | 85 | 86 | 91 | ``` 92 | 93 | ### 4. Using the `useMutation` hook 94 | 95 | ```vue 96 | 104 | 105 | 111 | ``` 112 | 113 | ## Usage with `trpc-nuxt` 114 | 115 | Setup `trpc-nuxt` as described in their [documentation](https://trpc-nuxt.vercel.app/get-started/usage/recommended). Then update the `plugins/client.ts` file: 116 | 117 | ```ts 118 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 119 | import { useQueryClient } from '@tanstack/vue-query' 120 | import { httpBatchLink } from 'trpc-nuxt/client' 121 | 122 | import type { AppRouter } from '~/server/trpc/routers' 123 | 124 | export default defineNuxtPlugin(() => { 125 | const queryClient = useQueryClient() 126 | 127 | // ⬇️ use `createTRPCVueQueryClient` instead of `createTRPCNuxtClient` ⬇️ 128 | const trpc = createTRPCVueQueryClient({ 129 | queryClient, 130 | trpc: { 131 | links: [ 132 | httpBatchLink({ 133 | url: '/api/trpc', 134 | }), 135 | ], 136 | }, 137 | }) 138 | 139 | return { 140 | provide: { 141 | trpc, 142 | }, 143 | } 144 | }) 145 | ``` 146 | 147 | ```ts 148 | export function useTRPC() { 149 | return useNuxtApp().$trpc 150 | } 151 | ``` 152 | 153 | ## Acknowledgements 154 | 155 | Huge thanks to [Robert Soriano](https://github.com/wobsoriano) for creating `trpc-nuxt`! We just adapted his work to work with Vue Query. 156 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@louishaftmann/commitlint-config'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | # Production license for @nuxt/ui-pro, get one at https://ui.nuxt.com/pro/purchase 2 | NUXT_UI_PRO_LICENSE= 3 | 4 | # Public URL, used for OG Image when running nuxt generate 5 | NUXT_PUBLIC_SITE_URL= 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | 26 | # VSC 27 | .history 28 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: 'blue', 4 | gray: 'slate', 5 | footer: { 6 | bottom: { 7 | left: 'text-sm text-gray-500 dark:text-gray-400', 8 | wrapper: 'border-t border-gray-200 dark:border-gray-800', 9 | }, 10 | }, 11 | }, 12 | seo: { 13 | siteName: 'tRPC Vue-Query', 14 | }, 15 | header: { 16 | logo: { 17 | alt: '', 18 | light: '', 19 | dark: '', 20 | }, 21 | search: true, 22 | colorMode: true, 23 | links: [ 24 | { 25 | 'icon': 'i-simple-icons-github', 26 | 'to': 'https://github.com/falcondev-oss/trpc-vue-query', 27 | 'target': '_blank', 28 | 'aria-label': 'tRPC Vue-Query on GitHub', 29 | }, 30 | ], 31 | }, 32 | footer: { 33 | credits: 'Copyright © 2024', 34 | colorMode: false, 35 | links: [ 36 | { 37 | 'icon': 'i-simple-icons-github', 38 | 'to': 'https://github.com/falcondev-oss/trpc-vue-query', 39 | 'target': '_blank', 40 | 'aria-label': 'tRPC Vue-Query on GitHub', 41 | }, 42 | ], 43 | }, 44 | toc: { 45 | title: 'Table of Contents', 46 | bottom: { 47 | title: 'Community', 48 | links: [ 49 | { 50 | icon: 'i-heroicons-star', 51 | label: 'Star on GitHub', 52 | to: 'https://github.com/falcondev-oss/trpc-vue-query', 53 | target: '_blank', 54 | }, 55 | ], 56 | }, 57 | }, 58 | }) 59 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /docs/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /docs/components/Header.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | -------------------------------------------------------------------------------- /docs/components/OgImage/OgImageDocs.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/1.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Install and setup tRPC Vue-Query 4 | --- 5 | 6 | ## Install 7 | 8 | ::code-group 9 | 10 | ```bash [pnpm] 11 | pnpm add @falcondev-oss/trpc-vue-query 12 | ``` 13 | 14 | ```bash [yarn] 15 | yarn add @falcondev-oss/trpc-vue-query 16 | ``` 17 | 18 | ```bash [bun] 19 | bun add @falcondev-oss/trpc-vue-query 20 | ``` 21 | 22 | ```bash [npm] 23 | npm install @falcondev-oss/trpc-vue-query 24 | ``` 25 | 26 | :: 27 | 28 | ## Setup with Vue 3 29 | 30 | ```ts [main.ts] 31 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 32 | import { VueQueryPlugin, useQueryClient } from '@tanstack/vue-query' 33 | import { httpBatchLink } from '@trpc/client' 34 | import type { AppRouter } from '../your_server/trpc' 35 | 36 | import type { AppRouter } from '../your_server/trpc' 37 | 38 | app.use(VueQueryPlugin) 39 | app.use({ 40 | install(app) { 41 | const queryClient = app.runWithContext(useQueryClient) 42 | const trpc = createTRPCVueQueryClient({ 43 | queryClient, 44 | trpc: { 45 | links: [ 46 | httpBatchLink({ 47 | url: '/api/trpc', 48 | }), 49 | ], 50 | }, 51 | }) 52 | 53 | app.provide('trpc', trpc) 54 | }, 55 | }) 56 | ``` 57 | 58 | ```ts [composables/useTRPC.ts] 59 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 60 | 61 | import type { AppRouter } from '../your_server/trpc' 62 | 63 | export function useTRPC() { 64 | return inject('trpc') as ReturnType> 65 | } 66 | ``` 67 | 68 | ## Setup with Nuxt 3 69 | 70 | Setup `trpc-nuxt` as described in their [documentation](https://trpc-nuxt.vercel.app/get-started/usage/recommended). Then update the `plugins/client.ts` file: 71 | 72 | ```ts [plugins/client.ts] 73 | import { createTRPCVueQueryClient } from '@falcondev-oss/trpc-vue-query' 74 | import { useQueryClient } from '@tanstack/vue-query' 75 | import { httpBatchLink } from 'trpc-nuxt/client' 76 | 77 | import type { AppRouter } from '~/server/trpc/routers' 78 | 79 | export default defineNuxtPlugin(() => { 80 | const queryClient = useQueryClient() 81 | 82 | // ⬇️ use `createTRPCVueQueryClient` instead of `createTRPCNuxtClient` ⬇️ 83 | const trpc = createTRPCVueQueryClient({ 84 | queryClient, 85 | trpc: { 86 | links: [ 87 | httpBatchLink({ 88 | url: '/api/trpc', 89 | }), 90 | ], 91 | }, 92 | }) 93 | 94 | return { 95 | provide: { 96 | trpc, 97 | }, 98 | } 99 | }) 100 | ``` 101 | 102 | ```ts [composables/useTRPC.ts] 103 | export function useTRPC() { 104 | return useNuxtApp().$trpc 105 | } 106 | ``` 107 | 108 | ## Acknowledgements 109 | 110 | Huge thanks to [Robert Soriano](https://github.com/wobsoriano) for creating `nuxt-trpc`! We just adapted his work to work with Vue Query. 111 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/2.usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: The basics of using tRPC Vue Query 4 | --- 5 | 6 | ## Querying data 7 | 8 | ### Basic usage 9 | 10 | ```vue 11 | 14 | ``` 15 | 16 | ### With parameters 17 | 18 | You can pass input parameters directly to the `useQuery` function. These parameters can also be reactive. 19 | 20 | ```vue 21 | 25 | ``` 26 | 27 | ### With options 28 | 29 | You can pass options as the second argument to the `useQuery` function. 30 | 31 | ```vue 32 | 37 | ``` 38 | 39 | ## Mutating data 40 | 41 | ```vue 42 | 53 | ``` 54 | 55 | ## Helpers 56 | 57 | ### `invalidate()` 58 | 59 | You can invalidate a query by calling the `invalidate` function. 60 | 61 | ```vue 62 | 71 | ``` 72 | 73 | ### `setQueryData()` 74 | 75 | You can update the query data manually by calling the `setQueryData` function. 76 | 77 | ```vue 78 | 86 | ``` 87 | 88 | ### `key()` 89 | 90 | You can get the query key by calling the `key` function. With the key you can access all the other Tanstack Query features. 91 | 92 | ```vue 93 | 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/content/1.getting-started/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Getting Started 2 | -------------------------------------------------------------------------------- /docs/content/composables/1.useQuery.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useQuery() 3 | description: The useQuery composable is a wrapper around the useQuery function from the Vue Query library. It provides a way to fetch data from an API and manage the state of the query. 4 | --- 5 | 6 | ## Basic usage 7 | 8 | ```vue 9 | 12 | ``` 13 | 14 | ## With parameters 15 | 16 | You can pass input parameters directly to the `useQuery` function. These parameters can also be reactive. 17 | 18 | ```vue 19 | 23 | ``` 24 | 25 | ## With options 26 | 27 | You can pass any `@tanstack/vue-query` options as the second argument to the `useQuery` function. 28 | 29 | ```vue 30 | 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/content/composables/2.useMutation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useMutation() 3 | description: The useMutation composable is a wrapper around the useMutation function from the Vue Query library. It provides a way to mutate data on the server and manage the state of the mutation. 4 | --- 5 | 6 | ```vue 7 | 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/content/composables/3.invalidate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: invalidate() 3 | description: You can invalidate a query by calling the invalidate() function. 4 | --- 5 | 6 | ```vue 7 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/content/composables/4.setQueryData.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: setQueryData() 3 | description: You can set the query data manually by calling the setQueryData() function. 4 | --- 5 | 6 | ```vue 7 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/content/composables/5.key.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: key() 3 | description: You can get the query key by calling the key() function. With the key you can access all the other Tanstack Query features. 4 | --- 5 | 6 | ```vue 7 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/content/composables/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Composables 2 | -------------------------------------------------------------------------------- /docs/content/index.yml: -------------------------------------------------------------------------------- 1 | title: tRPC Vue-Query 2 | description: Fully type-safe composables and helpers to make working with tRPC + Tanstack Query as intuitive as possible. 3 | navigation: false 4 | hero: 5 | title: tRPC Vue-Query 6 | description: Fully type-safe composables and helpers to make working with tRPC + Tanstack Query as intuitive as possible. 7 | orientation: vertical 8 | links: 9 | - label: Get started 10 | icon: i-heroicons-arrow-right-20-solid 11 | trailing: true 12 | to: /getting-started 13 | size: lg 14 | code: | 15 | ```vue 16 | 27 | ``` 28 | features: 29 | links: 30 | items: 31 | - title: Nuxt 3 or Vue 3 32 | description: Compatible with both trpc-nuxt module and Vue 3. 33 | icon: i-simple-icons-nuxtdotjs 34 | to: 'https://trpc-nuxt.vercel.app' 35 | target: _blank 36 | - title: TypeScript 37 | description: The fully typed development experience tRPC is known and loved for. 38 | icon: i-simple-icons-typescript 39 | to: 'https://www.typescriptlang.org' 40 | target: _blank 41 | - title: Helper Composables 42 | description: Full set helpers for common Tanstack Query operations. 43 | icon: i-heroicons-bolt-solid 44 | to: '/getting-started/usage#helpers' 45 | target: _blank 46 | -------------------------------------------------------------------------------- /docs/error.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 53 | -------------------------------------------------------------------------------- /docs/layouts/docs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | extends: ['@nuxt/ui-pro'], 4 | modules: ['@nuxt/content', '@nuxt/ui', '@nuxt/fonts', '@nuxthq/studio'], 5 | hooks: { 6 | // Define `@nuxt/ui` components as global to use them in `.md` (feel free to add those you need) 7 | 'components:extend': (components) => { 8 | const globals = components.filter((c) => ['UButton', 'UIcon'].includes(c.pascalName)) 9 | 10 | for (const c of globals) { 11 | c.global = true 12 | } 13 | }, 14 | }, 15 | app: { 16 | head: { 17 | link: [ 18 | { 19 | rel: 'icon', 20 | type: 'image/svg+xml', 21 | href: '/dna.svg', 22 | }, 23 | ], 24 | }, 25 | }, 26 | ui: { 27 | icons: ['heroicons', 'simple-icons'], 28 | }, 29 | nitro: { 30 | prerender: { 31 | crawlLinks: true, 32 | routes: ['/'], 33 | }, 34 | preset: 'cloudflare-pages', 35 | }, 36 | routeRules: { 37 | '/api/search.json': { prerender: true }, 38 | }, 39 | devtools: { 40 | enabled: true, 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /docs/nuxt.schema.ts: -------------------------------------------------------------------------------- 1 | import { field, group } from '@nuxthq/studio/theme' 2 | 3 | export default defineNuxtSchema({ 4 | appConfig: { 5 | ui: group({ 6 | title: 'UI', 7 | description: 'UI Customization.', 8 | icon: 'i-mdi-palette-outline', 9 | fields: { 10 | icons: group({ 11 | title: 'Icons', 12 | description: 'Manage icons used in UI Pro.', 13 | icon: 'i-mdi-application-settings-outline', 14 | fields: { 15 | search: field({ 16 | type: 'icon', 17 | title: 'Search Bar', 18 | description: 'Icon to display in the search bar.', 19 | icon: 'i-mdi-magnify', 20 | default: 'i-heroicons-magnifying-glass-20-solid', 21 | }), 22 | dark: field({ 23 | type: 'icon', 24 | title: 'Dark mode', 25 | description: 'Icon of color mode button for dark mode.', 26 | icon: 'i-mdi-moon-waning-crescent', 27 | default: 'i-heroicons-moon-20-solid', 28 | }), 29 | light: field({ 30 | type: 'icon', 31 | title: 'Light mode', 32 | description: 'Icon of color mode button for light mode.', 33 | icon: 'i-mdi-white-balance-sunny', 34 | default: 'i-heroicons-sun-20-solid', 35 | }), 36 | external: field({ 37 | type: 'icon', 38 | title: 'External Link', 39 | description: 'Icon for external link.', 40 | icon: 'i-mdi-arrow-top-right', 41 | default: 'i-heroicons-arrow-up-right-20-solid', 42 | }), 43 | chevron: field({ 44 | type: 'icon', 45 | title: 'Chevron', 46 | description: 'Icon for chevron.', 47 | icon: 'i-mdi-chevron-down', 48 | default: 'i-heroicons-chevron-down-20-solid', 49 | }), 50 | hash: field({ 51 | type: 'icon', 52 | title: 'Hash', 53 | description: 'Icon for hash anchors.', 54 | icon: 'i-ph-hash', 55 | default: 'i-heroicons-hashtag-20-solid', 56 | }), 57 | }, 58 | }), 59 | primary: field({ 60 | type: 'string', 61 | title: 'Primary', 62 | description: 'Primary color of your UI.', 63 | icon: 'i-mdi-palette-outline', 64 | default: 'green', 65 | required: [ 66 | 'sky', 67 | 'mint', 68 | 'rose', 69 | 'amber', 70 | 'violet', 71 | 'emerald', 72 | 'fuchsia', 73 | 'indigo', 74 | 'lime', 75 | 'orange', 76 | 'pink', 77 | 'purple', 78 | 'red', 79 | 'teal', 80 | 'yellow', 81 | 'green', 82 | 'blue', 83 | 'cyan', 84 | 'gray', 85 | 'white', 86 | 'black', 87 | ], 88 | }), 89 | gray: field({ 90 | type: 'string', 91 | title: 'Gray', 92 | description: 'Gray color of your UI.', 93 | icon: 'i-mdi-palette-outline', 94 | default: 'slate', 95 | required: ['slate', 'cool', 'zinc', 'neutral', 'stone'], 96 | }), 97 | }, 98 | }), 99 | seo: group({ 100 | title: 'SEO', 101 | description: 'SEO configuration.', 102 | icon: 'i-ph-app-window', 103 | fields: { 104 | siteName: field({ 105 | type: 'string', 106 | title: 'Site Name', 107 | description: 108 | 'Name used in ogSiteName and used as second part of your page title (My page title - Nuxt UI Pro).', 109 | icon: 'i-mdi-web', 110 | default: [], 111 | }), 112 | }, 113 | }), 114 | header: group({ 115 | title: 'Header', 116 | description: 'Header configuration.', 117 | icon: 'i-mdi-page-layout-header', 118 | fields: { 119 | logo: group({ 120 | title: 'Logo', 121 | description: 'Header logo configuration.', 122 | icon: 'i-mdi-image-filter-center-focus-strong-outline', 123 | fields: { 124 | light: field({ 125 | type: 'media', 126 | title: 'Light Mode Logo', 127 | description: 'Pick an image from your gallery.', 128 | icon: 'i-mdi-white-balance-sunny', 129 | default: '', 130 | }), 131 | dark: field({ 132 | type: 'media', 133 | title: 'Dark Mode Logo', 134 | description: 'Pick an image from your gallery.', 135 | icon: 'i-mdi-moon-waning-crescent', 136 | default: '', 137 | }), 138 | alt: field({ 139 | type: 'string', 140 | title: 'Alt', 141 | description: 'Alt to display for accessibility.', 142 | icon: 'i-mdi-alphabet-latin', 143 | default: '', 144 | }), 145 | }, 146 | }), 147 | search: field({ 148 | type: 'boolean', 149 | title: 'Search Bar', 150 | description: 'Hide or display the search bar.', 151 | icon: 'i-mdi-magnify', 152 | default: true, 153 | }), 154 | colorMode: field({ 155 | type: 'boolean', 156 | title: 'Color Mode', 157 | description: 'Hide or display the color mode button in your header.', 158 | icon: 'i-mdi-moon-waning-crescent', 159 | default: true, 160 | }), 161 | links: field({ 162 | type: 'array', 163 | title: 'Links', 164 | description: 'Array of link object displayed in header.', 165 | icon: 'i-mdi-link-variant', 166 | default: [], 167 | }), 168 | }, 169 | }), 170 | footer: group({ 171 | title: 'Footer', 172 | description: 'Footer configuration.', 173 | icon: 'i-mdi-page-layout-footer', 174 | fields: { 175 | credits: field({ 176 | type: 'string', 177 | title: 'Footer credits section', 178 | description: 'Text to display as credits in the footer.', 179 | icon: 'i-mdi-circle-edit-outline', 180 | default: '', 181 | }), 182 | colorMode: field({ 183 | type: 'boolean', 184 | title: 'Color Mode', 185 | description: 'Hide or display the color mode button in the footer.', 186 | icon: 'i-mdi-moon-waning-crescent', 187 | default: false, 188 | }), 189 | links: field({ 190 | type: 'array', 191 | title: 'Links', 192 | description: 'Array of link object displayed in footer.', 193 | icon: 'i-mdi-link-variant', 194 | default: [], 195 | }), 196 | }, 197 | }), 198 | toc: group({ 199 | title: 'Table of contents', 200 | description: 'TOC configuration.', 201 | icon: 'i-mdi-table-of-contents', 202 | fields: { 203 | title: field({ 204 | type: 'string', 205 | title: 'Title', 206 | description: 'Text to display as title of the main toc.', 207 | icon: 'i-mdi-format-title', 208 | default: '', 209 | }), 210 | bottom: group({ 211 | title: 'Bottom', 212 | description: 'Bottom TOC configuration.', 213 | icon: 'i-mdi-table-of-contents', 214 | fields: { 215 | title: field({ 216 | type: 'string', 217 | title: 'Title', 218 | description: 'Text to display as title of the bottom toc.', 219 | icon: 'i-mdi-format-title', 220 | default: '', 221 | }), 222 | edit: field({ 223 | type: 'string', 224 | title: 'Edit Page Link', 225 | description: 'URL of your repository content folder.', 226 | icon: 'i-ph-note-pencil', 227 | default: '', 228 | }), 229 | links: field({ 230 | type: 'array', 231 | title: 'Links', 232 | description: 'Array of link object displayed in bottom toc.', 233 | icon: 'i-mdi-link-variant', 234 | default: [], 235 | }), 236 | }, 237 | }), 238 | }, 239 | }), 240 | }, 241 | }) 242 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc-vue-query-docs", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@iconify-json/heroicons": "^1.2.1", 14 | "@iconify-json/simple-icons": "^1.2.12", 15 | "@nuxt/content": "^2.13.4", 16 | "@nuxt/fonts": "^0.10.2", 17 | "@nuxt/ui-pro": "^1.5.0", 18 | "nuxt": "^3.14.1592" 19 | }, 20 | "devDependencies": { 21 | "@nuxthq/studio": "^2.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/pages/[...slug].vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 78 | -------------------------------------------------------------------------------- /docs/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 60 | -------------------------------------------------------------------------------- /docs/public/dna.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/server/api/search.json.get.ts: -------------------------------------------------------------------------------- 1 | import { serverQueryContent } from '#content/server' 2 | 3 | export default eventHandler(async (event) => { 4 | return serverQueryContent(event) 5 | .where({ _type: 'markdown', navigation: { $ne: false } }) 6 | .find() 7 | }) 8 | -------------------------------------------------------------------------------- /docs/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme' 2 | 3 | import type { Config } from 'tailwindcss' 4 | 5 | export default >{ 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ['DM Sans', ...defaultTheme.fontFamily.sans], 10 | }, 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslintConfig from '@louishaftmann/eslint-config' 3 | 4 | export default eslintConfig({ 5 | nuxt: false, 6 | tsconfigPath: './tsconfig.json', 7 | }) 8 | .append({ 9 | rules: { 10 | 'no-console': 'off', 11 | }, 12 | }) 13 | .append({ 14 | ignores: [ 15 | '.prettierrc.cjs', 16 | '.lintstagedrc.mjs', 17 | 'node_modules/', 18 | 'dist/', 19 | '.nuxt/', 20 | '.output/', 21 | '.temp/', 22 | 'pnpm-lock.yaml', 23 | 'README.md/*.ts', 24 | 'docs/', 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@falcondev-oss/trpc-vue-query", 3 | "type": "module", 4 | "version": "0.5.0", 5 | "description": "A tRPC wrapper around '@tanstack/vue-query'", 6 | "license": "MIT", 7 | "repository": "github:falcondev-oss/trpc-vue-query", 8 | "bugs": { 9 | "url": "https://github.com/falcondev-oss/trpc-vue-query/issues" 10 | }, 11 | "keywords": [ 12 | "trpc", 13 | "vue-query", 14 | "trpc-client", 15 | "tanstack-query", 16 | "typescript" 17 | ], 18 | "exports": { 19 | "import": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dist/index.mjs" 22 | }, 23 | "require": { 24 | "types": "./dist/index.d.cts", 25 | "default": "./dist/index.cjs" 26 | } 27 | }, 28 | "main": "dist/index.cjs", 29 | "module": "dist/index.mjs", 30 | "types": "dist/index.d.ts", 31 | "files": [ 32 | "dist" 33 | ], 34 | "engines": { 35 | "node": "22", 36 | "pnpm": "10" 37 | }, 38 | "scripts": { 39 | "build": "tsup", 40 | "lint": "eslint --cache . && prettier --check --cache .", 41 | "lint:ci": "eslint --cache --cache-strategy content . && prettier --check --cache --cache-strategy content .", 42 | "lint:fix": "eslint --fix --cache . && prettier --write --cache .", 43 | "type-check": "tsc -p tsconfig.json --noEmit", 44 | "prepare": "husky", 45 | "test": "start-server-and-test test:server http-get://localhost:3000/ping vitest", 46 | "test:server": "tsx ./test/trpc/index.ts" 47 | }, 48 | "peerDependencies": { 49 | "@tanstack/vue-query": "^5.22.2", 50 | "@trpc/client": "^11", 51 | "@trpc/server": "^11", 52 | "vue": "^3.4.19" 53 | }, 54 | "dependencies": { 55 | "@vueuse/core": "^13.0.0" 56 | }, 57 | "devDependencies": { 58 | "@commitlint/cli": "^19.8.0", 59 | "@eslint/eslintrc": "^3.3.1", 60 | "@louishaftmann/commitlint-config": "^4.2.0", 61 | "@louishaftmann/eslint-config": "^4.2.0", 62 | "@louishaftmann/lintstaged-config": "^4.2.0", 63 | "@louishaftmann/prettier-config": "^4.2.0", 64 | "@tanstack/vue-query": "^5.71.10", 65 | "@trpc/client": "11.0.2", 66 | "@trpc/server": "11.0.2", 67 | "@types/eslint": "^9.6.1", 68 | "@types/ws": "^8.18.1", 69 | "@vitest/ui": "^3.1.1", 70 | "eslint": "^9.24.0", 71 | "husky": "^9.1.7", 72 | "lint-staged": "^15.5.0", 73 | "prettier": "^3.5.3", 74 | "start-server-and-test": "^2.0.11", 75 | "tsup": "^8.4.0", 76 | "tsx": "^4.19.3", 77 | "type-fest": "^4.39.1", 78 | "typescript": "^5.8.3", 79 | "vitest": "^3.1.1", 80 | "vue": "^3.5.13", 81 | "ws": "^8.18.1", 82 | "zod": "^3.24.2" 83 | }, 84 | "changelogithub": { 85 | "extends": "gh:falcondev-it/configs/changelogithub" 86 | }, 87 | "pnpm": { 88 | "onlyBuiltDependencies": [ 89 | "esbuild", 90 | "vue-demi" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/no-unsafe-argument */ 2 | /* eslint-disable ts/no-unsafe-assignment */ 3 | 4 | import type { 5 | InfiniteQueryPageParamsOptions, 6 | QueryClient, 7 | QueryFunction, 8 | SkipToken, 9 | } from '@tanstack/vue-query' 10 | import type { CreateTRPCClientOptions, TRPCUntypedClient } from '@trpc/client' 11 | import type { AnyTRPCRouter } from '@trpc/server' 12 | import type { MaybeRefOrGetter } from '@vueuse/core' 13 | import type { UnionToIntersection } from 'type-fest' 14 | import type { DecoratedProcedureRecord, DecorateProcedure } from './types' 15 | import { 16 | queryOptions as defineQueryOptions, 17 | skipToken, 18 | useInfiniteQuery, 19 | useMutation, 20 | useQueries, 21 | useQuery, 22 | } from '@tanstack/vue-query' 23 | import { createTRPCUntypedClient } from '@trpc/client' 24 | 25 | import { createTRPCFlatProxy } from '@trpc/server' 26 | import { createRecursiveProxy } from '@trpc/server/unstable-core-do-not-import' 27 | import { toRef, toRefs } from '@vueuse/core' 28 | import { computed, isReactive, onScopeDispose, shallowRef, toValue, watch } from 'vue' 29 | 30 | type QueryType = 'query' | 'queries' | 'infinite' 31 | export type TRPCQueryKey = [readonly string[], { input?: unknown; type?: QueryType }?] 32 | 33 | export { type Exact } from './types' 34 | 35 | function getQueryKey(path: string[], input: unknown, type?: QueryType): TRPCQueryKey { 36 | const splitPath = path.flatMap((part) => part.split('.')) 37 | 38 | if (input === undefined && !type) { 39 | return splitPath.length > 0 ? [splitPath] : ([] as unknown as TRPCQueryKey) 40 | } 41 | 42 | return [ 43 | splitPath, 44 | { 45 | ...(input !== undefined && input !== skipToken && { input }), 46 | ...(type && { type }), 47 | }, 48 | ] 49 | } 50 | 51 | function maybeToRefs(obj: MaybeRefOrGetter>) { 52 | // use https://vueuse.org/shared/toRefs to also support a ref of an object 53 | return isReactive(obj) ? toRefs(obj) : toRefs(toRef(obj)) 54 | } 55 | 56 | function createVueQueryProxyDecoration( 57 | name: string, 58 | trpc: TRPCUntypedClient, 59 | queryClient: QueryClient, 60 | ) { 61 | return createRecursiveProxy(({ args, path: _path }) => { 62 | const path = [name, ..._path] 63 | 64 | // The last arg is for instance `.useMutation` or `.useQuery` 65 | const prop = path.pop()! as keyof UnionToIntersection> | '_def' 66 | 67 | const joinedPath = path.join('.') 68 | const [firstArg, ...rest] = args 69 | const opts = rest[0] || ({} as any) 70 | 71 | if (prop === '_def') { 72 | return { path } 73 | } 74 | 75 | if (prop === 'query') { 76 | return trpc.query(joinedPath, firstArg, opts) 77 | } 78 | 79 | function createQuery( 80 | _input: MaybeRefOrGetter, 81 | { trpcOptions, queryOptions }: { trpcOptions: any; queryOptions: any }, 82 | { type = 'query' }: { type?: QueryType } = {}, 83 | ) { 84 | const queryFn = computed(() => 85 | toValue(_input) === skipToken 86 | ? skipToken 87 | : async ({ signal }) => { 88 | const input = toValue(_input) 89 | 90 | const output = await trpc.query(joinedPath, input, { 91 | signal, 92 | ...trpcOptions, 93 | }) 94 | 95 | if (type === 'queries') return { output, input } 96 | 97 | return output 98 | }, 99 | ) 100 | 101 | return defineQueryOptions({ 102 | queryKey: computed(() => getQueryKey(path, toValue(_input), type)), 103 | queryFn, 104 | ...maybeToRefs(queryOptions), 105 | }) 106 | } 107 | if (prop === 'useQuery') { 108 | const { trpc: trpcOptions, ...queryOptions } = opts 109 | const input = firstArg 110 | 111 | return useQuery(createQuery(input, { trpcOptions, queryOptions })) 112 | } 113 | 114 | if (prop === 'useQueries') { 115 | const { trpc: trpcOptions, combine, shallow, ...queryOptions } = opts 116 | const inputs = firstArg as MaybeRefOrGetter 117 | 118 | return useQueries({ 119 | queries: computed(() => 120 | toValue(inputs).map((i) => 121 | createQuery(i, { trpcOptions, queryOptions }, { type: 'queries' }), 122 | ), 123 | ), 124 | combine, 125 | ...maybeToRefs({ shallow }), 126 | }) 127 | } 128 | 129 | if (prop === 'invalidate') { 130 | return queryClient.invalidateQueries({ 131 | queryKey: getQueryKey(path, toValue(firstArg), 'query'), 132 | }) 133 | } 134 | 135 | if (prop === 'setQueryData') { 136 | return queryClient.setQueryData(getQueryKey(path, toValue(opts), 'query'), firstArg) 137 | } 138 | 139 | if (prop === 'key') { 140 | return getQueryKey(path, toValue(firstArg), 'query') 141 | } 142 | 143 | if (prop === 'mutate') { 144 | return trpc.mutation(joinedPath, firstArg, opts) 145 | } 146 | if (prop === 'useMutation') { 147 | const { trpc: trpcOptions, ...mutationOptions } = firstArg || ({} as any) 148 | 149 | return useMutation({ 150 | mutationKey: computed(() => getQueryKey(path, undefined)), 151 | mutationFn: async (payload) => 152 | trpc.mutation(joinedPath, payload, { 153 | ...trpcOptions, 154 | }), 155 | ...maybeToRefs(mutationOptions), 156 | }) 157 | } 158 | 159 | if (prop === 'subscribe') { 160 | return trpc.subscription(joinedPath, firstArg, opts) 161 | } 162 | if (prop === 'useSubscription') { 163 | const inputData = toRef(firstArg) 164 | 165 | const subscription = shallowRef>() 166 | watch( 167 | inputData, 168 | () => { 169 | if (inputData.value === skipToken) return 170 | 171 | subscription.value?.unsubscribe() 172 | 173 | subscription.value = trpc.subscription(joinedPath, inputData.value, { 174 | ...opts, 175 | }) 176 | }, 177 | { immediate: true }, 178 | ) 179 | 180 | onScopeDispose(() => { 181 | subscription.value?.unsubscribe() 182 | }, true) 183 | 184 | return subscription.value! 185 | } 186 | 187 | if (prop === 'useInfiniteQuery') { 188 | const { trpc: trpcOptions, ...queryOptions } = opts 189 | 190 | return useInfiniteQuery({ 191 | queryKey: computed(() => getQueryKey(path, toValue(firstArg), 'infinite')), 192 | queryFn: async ({ queryKey, pageParam, signal }) => 193 | trpc.query( 194 | joinedPath, 195 | { 196 | ...(queryKey[1]?.input as object), 197 | cursor: pageParam, 198 | }, 199 | { 200 | signal, 201 | ...trpcOptions, 202 | }, 203 | ), 204 | ...(maybeToRefs(queryOptions) as InfiniteQueryPageParamsOptions), 205 | }) 206 | } 207 | 208 | // return (trpc as any)[joinedPath][prop](...args) 209 | throw new Error(`Method '.${prop as string}()' not supported`) 210 | }) 211 | } 212 | 213 | export function createTRPCVueQueryClient({ 214 | trpc, 215 | queryClient, 216 | }: { 217 | queryClient: QueryClient 218 | trpc: CreateTRPCClientOptions 219 | }) { 220 | const client = createTRPCUntypedClient(trpc) 221 | 222 | const decoratedClient = createTRPCFlatProxy< 223 | DecoratedProcedureRecord 224 | >((key) => { 225 | return createVueQueryProxyDecoration(key.toString(), client, queryClient) 226 | }) 227 | 228 | return decoratedClient 229 | } 230 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InfiniteData, 3 | InitialPageParam, 4 | QueryClient, 5 | QueryKey, 6 | SkipToken, 7 | UseInfiniteQueryOptions, 8 | UseInfiniteQueryReturnType, 9 | UseMutationOptions, 10 | UseMutationReturnType, 11 | UseQueriesResults, 12 | UseQueryOptions, 13 | UseQueryReturnType, 14 | } from '@tanstack/vue-query' 15 | import type { OperationContext, TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client' 16 | import type { 17 | AnyTRPCMutationProcedure, 18 | AnyTRPCProcedure, 19 | AnyTRPCQueryProcedure, 20 | AnyTRPCRouter, 21 | AnyTRPCSubscriptionProcedure, 22 | inferProcedureInput, 23 | inferProcedureOutput, 24 | inferTransformedProcedureOutput, 25 | } from '@trpc/server' 26 | import type { Unsubscribable } from '@trpc/server/observable' 27 | import type { MaybeRefOrGetter, Ref, UnwrapRef } from 'vue' 28 | 29 | type inferAsyncIterableYield = T extends AsyncIterable ? U : T 30 | 31 | type TRPCSubscriptionObserver = { 32 | onStarted: (opts: { context: OperationContext | undefined }) => void 33 | onData: (value: inferAsyncIterableYield) => void 34 | onError: (err: TError) => void 35 | onStopped: () => void 36 | onComplete: () => void 37 | } 38 | 39 | type ArrayElement = T extends readonly unknown[] ? T[number] : never 40 | type Primitive = null | undefined | string | number | boolean | symbol | bigint 41 | export type Exact = Shape extends Primitive 42 | ? Shape 43 | : Shape extends object 44 | ? { 45 | [Key in keyof T]: Key extends keyof Shape 46 | ? T[Key] extends Date 47 | ? T[Key] 48 | : T[Key] extends unknown[] 49 | ? Array, ArrayElement>> 50 | : T[Key] extends readonly unknown[] 51 | ? ReadonlyArray, ArrayElement>> 52 | : T[Key] extends object 53 | ? Exact 54 | : T[Key] 55 | : never 56 | } 57 | : Shape 58 | 59 | // TODO: extract subtypes and use them as satisfies checks in index.ts 60 | export type DecorateProcedure< 61 | TProcedure extends AnyTRPCProcedure, 62 | TRouter extends AnyTRPCRouter, 63 | > = TProcedure extends AnyTRPCQueryProcedure 64 | ? { 65 | useQuery: < 66 | TQueryFnData extends inferTransformedProcedureOutput, 67 | TError extends TRPCClientErrorLike, 68 | TData extends TQueryFnData, 69 | TQueryData extends TQueryFnData, 70 | TQueryKey extends QueryKey, 71 | TInput extends inferProcedureInput, 72 | >( 73 | input: inferProcedureInput extends void 74 | ? inferProcedureInput 75 | : 76 | | Ref, TInput> | SkipToken> 77 | | (() => Exact, TInput> | SkipToken), 78 | opts?: MaybeRefOrGetter< 79 | Omit< 80 | UnwrapRef>, 81 | 'queryKey' 82 | > & { 83 | trpc?: TRPCRequestOptions 84 | queryKey?: TQueryKey 85 | } 86 | >, 87 | ) => UseQueryReturnType 88 | useQueries: < 89 | TQueryFnData extends { 90 | output: inferTransformedProcedureOutput 91 | input: inferProcedureInput 92 | }, 93 | TError extends TRPCClientErrorLike, 94 | TData extends TQueryFnData, 95 | TQueryData extends TQueryFnData, 96 | TQueryKey extends QueryKey, 97 | TInput extends inferProcedureInput, 98 | TQueries extends UseQueryOptions, 99 | TCombinedResult = UseQueriesResults, 100 | >( 101 | inputs: MaybeRefOrGetter, TInput>[]>, 102 | opts?: MaybeRefOrGetter< 103 | Omit, 'queryKey'> & { 104 | trpc?: TRPCRequestOptions 105 | queryKey?: never 106 | combine?: (result: UseQueriesResults) => TCombinedResult 107 | shallow?: boolean 108 | } 109 | >, 110 | ) => Readonly> 111 | query: >( 112 | input: Exact, TInput>, 113 | opts?: TRPCRequestOptions, 114 | ) => Promise> 115 | invalidate: >( 116 | input?: MaybeRefOrGetter, TInput>>, 117 | ) => Promise 118 | setQueryData: >( 119 | updater: inferTransformedProcedureOutput, 120 | input?: MaybeRefOrGetter, TInput>>, 121 | ) => ReturnType 122 | key: >( 123 | input?: MaybeRefOrGetter, TInput>>, 124 | ) => QueryKey 125 | } & (TProcedure['_def']['$types']['input'] extends { cursor?: infer CursorType } 126 | ? { 127 | useInfiniteQuery: < 128 | TQueryFnData extends inferTransformedProcedureOutput, 129 | TError extends TRPCClientErrorLike, 130 | TData extends InfiniteData, 131 | TQueryData extends TQueryFnData, 132 | TQueryKey extends QueryKey, 133 | TInput extends inferProcedureInput, 134 | >( 135 | input: MaybeRefOrGetter, 'cursor'>, TInput>>, 136 | opts?: MaybeRefOrGetter< 137 | Omit< 138 | UnwrapRef< 139 | UseInfiniteQueryOptions 140 | >, 141 | 'queryKey' | keyof InitialPageParam 142 | > & { 143 | trpc?: TRPCRequestOptions 144 | queryKey?: TQueryKey 145 | } & (undefined extends TProcedure['_def']['$types']['input']['cursor'] 146 | ? Partial> 147 | : InitialPageParam) 148 | >, 149 | ) => UseInfiniteQueryReturnType 150 | } 151 | : object) 152 | : TProcedure extends AnyTRPCMutationProcedure 153 | ? { 154 | mutate: >( 155 | input: Exact, TInput>, 156 | opts?: TRPCRequestOptions, 157 | ) => Promise> 158 | useMutation: < 159 | TData = inferTransformedProcedureOutput, 160 | TError = TRPCClientErrorLike, 161 | TVariables = inferProcedureInput, 162 | TContext = unknown, 163 | >( 164 | opts?: MaybeRefOrGetter< 165 | UseMutationOptions & { 166 | trpc?: TRPCRequestOptions 167 | } 168 | >, 169 | // for exact types, patch @tanstack/query-core `MutateFunction` 170 | ) => UseMutationReturnType 171 | } 172 | : TProcedure extends AnyTRPCSubscriptionProcedure 173 | ? { 174 | subscribe: >( 175 | input: Exact, TInput>, 176 | opts: TRPCRequestOptions & 177 | Partial< 178 | TRPCSubscriptionObserver< 179 | inferProcedureOutput, 180 | TRPCClientErrorLike 181 | > 182 | >, 183 | ) => Unsubscribable 184 | useSubscription: >( 185 | input: MaybeRefOrGetter, TInput>>, 186 | opts: TRPCRequestOptions & 187 | Partial< 188 | TRPCSubscriptionObserver< 189 | inferProcedureOutput, 190 | TRPCClientErrorLike 191 | > 192 | >, 193 | ) => Unsubscribable 194 | } 195 | : never 196 | 197 | /** 198 | * @internal 199 | */ 200 | export type DecoratedProcedureRecord< 201 | TProcedures extends Record, 202 | TRouter extends AnyTRPCRouter, 203 | > = { 204 | [K in keyof TProcedures]: TProcedures[K] extends AnyTRPCProcedure 205 | ? DecorateProcedure 206 | : DecoratedProcedureRecord 207 | } 208 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { keepPreviousData, skipToken } from '@tanstack/vue-query' 2 | import { until } from '@vueuse/core' 3 | import { describe, expect, test, vi } from 'vitest' 4 | import { ref } from 'vue' 5 | 6 | import { app, useTRPC } from './vue-app' 7 | 8 | test('query()', async () => { 9 | await app.runWithContext(async () => { 10 | const trpc = useTRPC() 11 | 12 | const pong = await trpc.hello.query({ name: 'World' }) 13 | 14 | expect(pong).toEqual('Hello World!') 15 | }) 16 | }) 17 | 18 | describe('useQuery()', async () => { 19 | await app.runWithContext(async () => { 20 | const trpc = useTRPC() 21 | 22 | const pong = trpc.hello.useQuery(() => ({ name: 'Pong' }), { 23 | placeholderData: keepPreviousData, 24 | suspense: true, 25 | }) 26 | 27 | await pong.suspense() 28 | 29 | expect(pong.data.value).toEqual('Hello Pong!') 30 | }) 31 | 32 | test('empty', async () => { 33 | await app.runWithContext(async () => { 34 | const trpc = useTRPC() 35 | 36 | const empty = trpc.emptyQuery.useQuery() 37 | 38 | await empty.suspense() 39 | 40 | expect(empty.data.value).toBeNull() 41 | 42 | const empty2 = await trpc.emptyQuery.query() 43 | expect(empty2).toBeNull() 44 | }) 45 | }) 46 | 47 | test('skipToken', async () => { 48 | await app.runWithContext(async () => { 49 | const trpc = useTRPC() 50 | 51 | const name = ref(undefined) 52 | const pong = trpc.hello.useQuery(() => (name.value ? { name: name.value } : skipToken)) 53 | 54 | expect(pong.fetchStatus.value).toStrictEqual('idle') 55 | expect(pong.data.value).toBeUndefined() 56 | 57 | name.value = 'World' 58 | expect(pong.status.value).toStrictEqual('pending') 59 | await vi.waitUntil(() => pong.status.value === 'success') 60 | 61 | expect(pong.data.value).toStrictEqual('Hello World!') 62 | }) 63 | }) 64 | }) 65 | 66 | test('useMutation()', async () => { 67 | await app.runWithContext(async () => { 68 | const trpc = useTRPC() 69 | 70 | const result = trpc.emptyMutation.useMutation() 71 | 72 | await result.mutateAsync() 73 | 74 | expect(result.data.value).toBeNull() 75 | }) 76 | }) 77 | 78 | test('mutate()', async () => { 79 | await app.runWithContext(async () => { 80 | const trpc = useTRPC() 81 | 82 | const result = await trpc.emptyMutation.mutate() 83 | 84 | expect(result).toBeNull() 85 | }) 86 | }) 87 | 88 | test('useInfiniteQuery()', async () => { 89 | await app.runWithContext(async () => { 90 | const trpc = useTRPC() 91 | 92 | const infinite = trpc.infinite.useInfiniteQuery( 93 | { limit: 10 }, 94 | { 95 | initialPageParam: 1, 96 | getNextPageParam: (lastPage) => lastPage.items.length + 1, 97 | placeholderData: keepPreviousData, 98 | suspense: true, 99 | }, 100 | ) 101 | 102 | await infinite.suspense() 103 | 104 | console.log(infinite.data.value?.pages) 105 | 106 | expect(infinite.data.value?.pages).toHaveLength(1) 107 | expect(infinite.data.value?.pageParams).toStrictEqual([1]) 108 | 109 | await infinite.fetchNextPage() 110 | 111 | expect(infinite.data.value?.pages).toHaveLength(2) 112 | expect(infinite.data.value?.pageParams).toStrictEqual([1, 11]) 113 | }) 114 | }) 115 | 116 | test('subscribe()', async () => { 117 | await app.runWithContext(async () => { 118 | const trpc = useTRPC() 119 | 120 | const mockFn = vi.fn() 121 | 122 | const subscription = trpc.count.subscribe( 123 | { max: 5 }, 124 | { 125 | onStarted() { 126 | console.log('Started') 127 | }, 128 | onData(value) { 129 | console.log('Data:', value) 130 | mockFn(value) 131 | }, 132 | onError(err) { 133 | console.error('Error:', err) 134 | }, 135 | onStopped() { 136 | console.log('Stopped') 137 | }, 138 | onComplete() { 139 | console.log('Completed') 140 | }, 141 | }, 142 | ) 143 | 144 | await new Promise((resolve) => setTimeout(resolve, 450)) 145 | 146 | expect(mockFn).toHaveBeenCalledTimes(5) 147 | expect(mockFn).toHaveBeenLastCalledWith(5) 148 | 149 | subscription.unsubscribe() 150 | }) 151 | }) 152 | 153 | test('useSubscription()', async () => { 154 | await app.runWithContext(async () => { 155 | const trpc = useTRPC() 156 | 157 | const mockFn = vi.fn() 158 | 159 | const input = ref({ max: 5 }) 160 | const subscription = trpc.count.useSubscription(input, { 161 | onStarted() { 162 | console.log('Started') 163 | }, 164 | onData(value) { 165 | console.log('Data:', value) 166 | mockFn(value) 167 | }, 168 | onError(err) { 169 | console.error('Error:', err) 170 | }, 171 | onStopped() { 172 | console.log('Stopped') 173 | }, 174 | onComplete() { 175 | console.log('Completed') 176 | }, 177 | }) 178 | 179 | await new Promise((resolve) => setTimeout(resolve, 450)) 180 | 181 | expect(mockFn).toHaveBeenCalledTimes(5) 182 | expect(mockFn).toHaveBeenLastCalledWith(5) 183 | 184 | subscription.unsubscribe() 185 | }) 186 | }) 187 | 188 | test('useQueries()', async () => { 189 | await app.runWithContext(async () => { 190 | const trpc = useTRPC() 191 | 192 | const hellos = Array.from({ length: 5 }, (_, i) => ({ name: i.toString() })) 193 | 194 | const queries = trpc.hello.useQueries(() => hellos, { 195 | suspense: true, 196 | combine: (results) => { 197 | return { 198 | data: results.map((result) => result.data), 199 | pending: results.some((result) => result.isPending), 200 | } 201 | }, 202 | }) 203 | 204 | await until(() => queries.value.pending).toBe(false) 205 | 206 | expect(queries.value.data.map((d) => d?.output)).toEqual( 207 | hellos.map((hello) => `Hello ${hello.name}!`), 208 | ) 209 | }) 210 | }) 211 | -------------------------------------------------------------------------------- /test/trpc/index.ts: -------------------------------------------------------------------------------- 1 | import { createHTTPServer } from '@trpc/server/adapters/standalone' 2 | import { applyWSSHandler } from '@trpc/server/adapters/ws' 3 | import { WebSocketServer } from 'ws' 4 | import { z } from 'zod' 5 | 6 | import { publicProcedure, router } from './trpc' 7 | 8 | export const appRouter = router({ 9 | ping: publicProcedure.query(() => 'Pong!'), 10 | hello: publicProcedure 11 | .input( 12 | z.object({ 13 | name: z.string(), 14 | }), 15 | ) 16 | .query(({ input }) => `Hello ${input.name}!`), 17 | 18 | emptyQuery: publicProcedure.query(() => null), 19 | emptyMutation: publicProcedure.mutation(() => null), 20 | 21 | count: publicProcedure 22 | .input( 23 | z.object({ 24 | max: z.number(), 25 | delayMs: z.number().optional().default(100), 26 | }), 27 | ) 28 | .subscription(async function* ({ input }) { 29 | let i = 1 30 | while (i <= input.max) { 31 | yield i 32 | i++ 33 | await new Promise((resolve) => setTimeout(resolve, input.delayMs)) 34 | } 35 | }), 36 | 37 | infinite: publicProcedure 38 | .input( 39 | z.object({ 40 | limit: z.number().min(1).max(100), 41 | cursor: z.number().default(0), 42 | }), 43 | ) 44 | .query(({ input }) => { 45 | return { 46 | items: Array.from({ length: input.limit }, (_, i) => i + input.cursor), 47 | } 48 | }), 49 | }) 50 | 51 | export type AppRouter = typeof appRouter 52 | 53 | const server = createHTTPServer({ 54 | router: appRouter, 55 | }) 56 | 57 | const wss = new WebSocketServer({ server }) 58 | applyWSSHandler({ 59 | wss, 60 | router: appRouter, 61 | }) 62 | 63 | server.listen(3000) 64 | console.log(`Listening on http://localhost:3000`) 65 | -------------------------------------------------------------------------------- /test/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server' 2 | 3 | const t = initTRPC.create() 4 | 5 | export const router = t.router 6 | export const publicProcedure = t.procedure 7 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["es2022"], 5 | "moduleDetection": "force", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "resolveJsonModule": true, 9 | "allowJs": true, 10 | "strict": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": [".", "../vitest.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /test/vue-app.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | import type { AppRouter } from './trpc/index' 3 | import { useQueryClient, VueQueryPlugin } from '@tanstack/vue-query' 4 | import { createWSClient, httpLink, splitLink, wsLink } from '@trpc/client' 5 | 6 | import { createApp, inject } from 'vue' 7 | 8 | import { WebSocket } from 'ws' 9 | import { createTRPCVueQueryClient } from '../src/index' 10 | 11 | const trpcKey = Symbol('trpc') as InjectionKey< 12 | ReturnType> 13 | > 14 | 15 | export const app = createApp({}) 16 | 17 | // eslint-disable-next-line ts/no-unsafe-assignment 18 | globalThis.WebSocket = WebSocket as any 19 | 20 | const apiUrl = 'http://localhost:3000/' 21 | const wsClient = createWSClient({ url: apiUrl.replace('http', 'ws') }) 22 | 23 | app.use(VueQueryPlugin) 24 | app.use({ 25 | install() { 26 | const queryClient = app.runWithContext(useQueryClient) 27 | const trpc = createTRPCVueQueryClient({ 28 | queryClient, 29 | trpc: { 30 | links: [ 31 | splitLink({ 32 | condition: (op) => op.type === 'subscription', 33 | true: wsLink({ 34 | client: wsClient, 35 | }), 36 | false: httpLink({ 37 | url: apiUrl, 38 | }), 39 | }), 40 | ], 41 | }, 42 | }) 43 | 44 | app.provide(trpcKey, trpc) 45 | }, 46 | }) 47 | 48 | export function useTRPC() { 49 | const trpc = inject(trpcKey) 50 | if (!trpc) throw new Error('tRPC client is not available.') 51 | 52 | return trpc 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["es2022"], 5 | "moduleDetection": "force", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "resolveJsonModule": true, 9 | "allowJs": true, 10 | "strict": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": ["src/", "*"], 18 | "exclude": ["docs/*/**"] 19 | } 20 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: false, 7 | clean: true, 8 | external: [/@trpc\/client/, /@trpc\/server/, 'vue'], 9 | dts: true, 10 | outExtension({ format }) { 11 | return { 12 | js: format === 'esm' ? '.mjs' : `.${format}`, 13 | } 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | // setupFiles: ['./test/setup.ts'], 6 | typecheck: { 7 | enabled: true, 8 | tsconfig: './test/tsconfig.json', 9 | }, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------