Skip to content

Commit 0ad6e77

Browse files
committed
feat: vue adapter
# Conflicts: # pnpm-lock.yaml
1 parent 556125d commit 0ad6e77

File tree

193 files changed

+9297
-365
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

193 files changed

+9297
-365
lines changed

docs/framework/vue/adapter.md

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
---
2+
title: TanStack Pacer Vue Adapter
3+
id: adapter
4+
---
5+
6+
If you are using TanStack Pacer in a Vue application, we recommend using the Vue Adapter. The Vue Adapter provides a set of easy-to-use composables on top of the core Pacer utilities. If you find yourself wanting to use the core Pacer classes/functions directly, the Vue Adapter will also re-export everything from the core package.
7+
8+
## Installation
9+
10+
```sh
11+
npm install @tanstack/vue-pacer
12+
```
13+
14+
## Vue Composables
15+
16+
See the [Vue Functions Reference](./reference/index.md) to see the full list of composables available in the Vue Adapter.
17+
18+
## Basic Usage
19+
20+
Import a Vue-specific composable from the Vue Adapter.
21+
22+
```vue
23+
<script setup lang="ts">
24+
import { ref } from 'vue'
25+
import { useDebouncedValue } from '@tanstack/vue-pacer'
26+
27+
const instantValue = ref(0)
28+
const { debouncedValue, debouncer } = useDebouncedValue(instantValue, {
29+
wait: 1000,
30+
})
31+
</script>
32+
```
33+
34+
Or import a core Pacer class/function that is re-exported from the Vue Adapter.
35+
36+
```ts
37+
import { debounce, Debouncer } from '@tanstack/vue-pacer' // no need to install the core package separately
38+
```
39+
40+
## Option Helpers
41+
42+
If you want a type-safe way to define common options for pacer utilities, TanStack Pacer provides option helpers for each utility. These helpers can be used with Vue composables.
43+
44+
### Debouncer Options
45+
46+
```vue
47+
<script setup lang="ts">
48+
import { useDebouncer } from '@tanstack/vue-pacer'
49+
import { debouncerOptions } from '@tanstack/pacer'
50+
51+
const commonDebouncerOptions = debouncerOptions({
52+
wait: 1000,
53+
leading: false,
54+
trailing: true,
55+
})
56+
57+
const debouncer = useDebouncer(
58+
(query: string) => fetchSearchResults(query),
59+
{ ...commonDebouncerOptions, key: 'searchDebouncer' }
60+
)
61+
</script>
62+
```
63+
64+
### Queuer Options
65+
66+
```vue
67+
<script setup lang="ts">
68+
import { useQueuer } from '@tanstack/vue-pacer'
69+
import { queuerOptions } from '@tanstack/pacer'
70+
71+
const commonQueuerOptions = queuerOptions({
72+
concurrency: 3,
73+
addItemsTo: 'back',
74+
})
75+
76+
const queuer = useQueuer(
77+
(item: string) => processItem(item),
78+
{ ...commonQueuerOptions, key: 'itemQueuer' }
79+
)
80+
</script>
81+
```
82+
83+
### Rate Limiter Options
84+
85+
```vue
86+
<script setup lang="ts">
87+
import { useRateLimiter } from '@tanstack/vue-pacer'
88+
import { rateLimiterOptions } from '@tanstack/pacer'
89+
90+
const commonRateLimiterOptions = rateLimiterOptions({
91+
limit: 5,
92+
window: 60000,
93+
windowType: 'sliding',
94+
})
95+
96+
const rateLimiter = useRateLimiter(
97+
(data: string) => sendApiRequest(data),
98+
{ ...commonRateLimiterOptions, key: 'apiRateLimiter' }
99+
)
100+
</script>
101+
```
102+
103+
## Provider
104+
105+
The Vue Adapter provides a `PacerProvider` using Vue's provide/inject pattern that you can use to provide default options to all instances of pacer utilities within your component tree.
106+
107+
```vue
108+
<script setup lang="ts">
109+
import { providePacerOptions } from '@tanstack/vue-pacer'
110+
111+
// Set default options for vue-pacer instances
112+
providePacerOptions({
113+
debouncer: { wait: 1000 },
114+
queuer: { concurrency: 3 },
115+
rateLimiter: { limit: 5, window: 60000 },
116+
})
117+
</script>
118+
119+
<template>
120+
<App />
121+
</template>
122+
```
123+
124+
All composables within the provider will automatically use these default options, which can be overridden on a per-composable basis.
125+
126+
## Subscribing to State
127+
128+
The Vue adapter supports subscribing to state changes in two ways:
129+
130+
### Using the Subscribe Component
131+
132+
Use the `Subscribe` component to subscribe to state changes deep in your component tree without needing to pass a selector to the composable. This is ideal when you want to subscribe to state in child components. The Subscribe component uses scoped slots.
133+
134+
```vue
135+
<script setup lang="ts">
136+
import { useRateLimiter } from '@tanstack/vue-pacer'
137+
138+
const rateLimiter = useRateLimiter(
139+
(data: string) => {
140+
return fetch('/api/endpoint', {
141+
method: 'POST',
142+
body: JSON.stringify({ data }),
143+
})
144+
},
145+
{ limit: 5, window: 60000 }
146+
)
147+
</script>
148+
149+
<template>
150+
<div>
151+
<button @click="rateLimiter.maybeExecute('some data')">
152+
Submit
153+
</button>
154+
155+
<rateLimiter.Subscribe
156+
:selector="(state) => ({ rejectionCount: state.rejectionCount })"
157+
v-slot="{ rejectionCount }"
158+
>
159+
<div>Rejections: {{ rejectionCount }}</div>
160+
</rateLimiter.Subscribe>
161+
</div>
162+
</template>
163+
```
164+
165+
### Using the Selector Parameter
166+
167+
The `selector` parameter allows you to specify which state changes will trigger reactive updates at the composable level, optimizing performance by preventing unnecessary updates when irrelevant state changes occur.
168+
169+
**By default, `composable.state` is empty (`{}`) as the selector is empty by default.** You must opt-in to state tracking by providing a selector function.
170+
171+
```vue
172+
<script setup lang="ts">
173+
import { useDebouncer } from '@tanstack/vue-pacer'
174+
175+
// Default behavior - no reactive state subscriptions
176+
const debouncer1 = useDebouncer(
177+
(query: string) => fetchSearchResults(query),
178+
{ wait: 500 }
179+
)
180+
console.log(debouncer1.state) // {}
181+
182+
// Opt-in to track isPending changes
183+
const debouncer2 = useDebouncer(
184+
(query: string) => fetchSearchResults(query),
185+
{ wait: 500 },
186+
(state) => ({ isPending: state.isPending })
187+
)
188+
console.log(debouncer2.state.isPending) // Reactive Ref value
189+
</script>
190+
191+
<template>
192+
<input
193+
@input="(e) => debouncer2.maybeExecute((e.target as HTMLInputElement).value)"
194+
placeholder="Search..."
195+
/>
196+
</template>
197+
```
198+
199+
For more details on state management and available state properties, see the individual guide pages for each utility (e.g., [Rate Limiting Guide](../../guides/rate-limiting.md), [Debouncing Guide](../../guides/debouncing.md)).
200+
201+
## Examples
202+
203+
### Debouncer Example
204+
205+
```vue
206+
<script setup lang="ts">
207+
import { useDebouncer } from '@tanstack/vue-pacer'
208+
209+
const debouncer = useDebouncer(
210+
(query: string) => {
211+
console.log('Searching for:', query)
212+
// Perform search
213+
},
214+
{ wait: 500 }
215+
)
216+
</script>
217+
218+
<template>
219+
<input
220+
@input="(e) => debouncer.maybeExecute((e.target as HTMLInputElement).value)"
221+
placeholder="Search..."
222+
/>
223+
</template>
224+
```
225+
226+
### Queuer Example
227+
228+
```vue
229+
<script setup lang="ts">
230+
import { useQueuer } from '@tanstack/vue-pacer'
231+
232+
const queuer = useQueuer(
233+
async (file: File) => {
234+
await uploadFile(file)
235+
},
236+
{ concurrency: 3 }
237+
)
238+
239+
function handleFileSelect(files: FileList) {
240+
Array.from(files).forEach((file) => {
241+
queuer.addItem(file)
242+
})
243+
}
244+
</script>
245+
246+
<template>
247+
<input
248+
type="file"
249+
multiple
250+
@change="(e) => {
251+
const target = e.target as HTMLInputElement
252+
if (target.files) {
253+
handleFileSelect(target.files)
254+
}
255+
}"
256+
/>
257+
</template>
258+
```
259+
260+
### Rate Limiter Example
261+
262+
```vue
263+
<script setup lang="ts">
264+
import { useRateLimiter } from '@tanstack/vue-pacer'
265+
266+
const rateLimiter = useRateLimiter(
267+
(data: string) => {
268+
return fetch('/api/endpoint', {
269+
method: 'POST',
270+
body: JSON.stringify({ data }),
271+
})
272+
},
273+
{
274+
limit: 5,
275+
window: 60000,
276+
windowType: 'sliding',
277+
onReject: () => {
278+
alert('Rate limit reached. Please try again later.')
279+
},
280+
}
281+
)
282+
283+
function handleSubmit() {
284+
const remaining = rateLimiter.getRemainingInWindow()
285+
if (remaining > 0) {
286+
rateLimiter.maybeExecute('some data')
287+
}
288+
}
289+
</script>
290+
291+
<template>
292+
<button @click="handleSubmit">Submit</button>
293+
</template>
294+
```
295+
296+
### Debounced Ref Example
297+
298+
```vue
299+
<script setup lang="ts">
300+
import { useDebouncedRef } from '@tanstack/vue-pacer'
301+
302+
const { value: searchText, debouncedValue, debouncer } = useDebouncedRef('', {
303+
wait: 500,
304+
})
305+
</script>
306+
307+
<template>
308+
<div>
309+
<input v-model="searchText" placeholder="Search..." />
310+
<p>Instant: {{ searchText }}</p>
311+
<p>Debounced: {{ debouncedValue }}</p>
312+
313+
<debouncer.Subscribe
314+
:selector="(state) => state"
315+
v-slot="state"
316+
>
317+
<pre>{{ JSON.stringify(state, null, 2) }}</pre>
318+
</debouncer.Subscribe>
319+
</div>
320+
</template>
321+
```

examples/vue/debounce/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="theme-color" content="#000000" />
8+
<title>TanStack Pacer Example</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
<script type="module" src="/src/index.ts"></script>
14+
</body>
15+
</html>

examples/vue/debounce/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@tanstack/pacer-example-vue-debounce",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite --port=3005",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "vue-tsc --noEmit"
10+
},
11+
"dependencies": {
12+
"@tanstack/vue-pacer": "^0.19.1",
13+
"vue": "^3.5.16"
14+
},
15+
"devDependencies": {
16+
"@vitejs/plugin-vue": "^5.2.3",
17+
"vite": "^7.3.1",
18+
"vue-tsc": "^2.2.0"
19+
},
20+
"browserslist": {
21+
"production": [
22+
">0.2%",
23+
"not dead",
24+
"not op_mini all"
25+
],
26+
"development": [
27+
"last 1 chrome version",
28+
"last 1 firefox version",
29+
"last 1 safari version"
30+
]
31+
}
32+
}

0 commit comments

Comments
 (0)