Skip to content

Instantly share code, notes, and snippets.

@wobsoriano
Last active January 6, 2025 08:48
Show Gist options
  • Save wobsoriano/9a7de2d2aaf9448c2fb952d2746b6907 to your computer and use it in GitHub Desktop.
Save wobsoriano/9a7de2d2aaf9448c2fb952d2746b6907 to your computer and use it in GitHub Desktop.
TanStack Query + Vue Options API
<script lang="ts">
import { defineComponent, toRaw } from 'vue'
import {
QueryObserver,
type QueryKey,
type QueryObserverResult,
type QueryClient,
} from '@tanstack/query-core'
type Todo = {
userId: number
id: number
title: string
completed: boolean
}
export default defineComponent({
inject: ['queryClient'],
data: () => ({
todoId: 1,
result: {} as QueryObserverResult<Todo, unknown>,
observer: null as null | QueryObserver<Todo, unknown, Todo, Todo, QueryKey>,
unsubscribe: () => {}
}),
methods: {
async fetchTodo(id: number) {
const resp = await fetch('https://jsonplaceholder.typicode.com/todos/' + id)
const data = await resp.json()
return data
},
},
mounted() {
this.observer = new QueryObserver<Todo, unknown, Todo, Todo, QueryKey>(this.queryClient as QueryClient, {
queryKey: ['todo', 1],
queryFn: () => this.fetchTodo(1),
})
this.unsubscribe = this.observer.subscribe((result) => {
Object.keys(result).forEach((key) => {
// @ts-expect-error: Incompatible types
this.result[key] = result[key]
})
})
},
beforeUnmount() {
this.unsubscribe()
},
watch: {
todoId(id) {
toRaw(this.observer)?.setOptions({
queryKey: ['todo', id],
queryFn: () => this.fetchTodo(id),
})
}
}
})
</script>
<template>
<main>
<div v-if="result.isLoading">Loading...</div>
<div v-else>{{ JSON.stringify(result.data) }}</div>
<button @click="todoId++" :disabled="result.isLoading">
{{ result.isLoading ? 'Fetching...' : 'Next todo' }}
</button>
</main>
</template>
import { createApp } from 'vue'
import App from './App.vue'
import { QueryClient } from '@tanstack/query-core'
const app = createApp(App)
const queryClient = new QueryClient()
app.provide('queryClient', queryClient)
app.mount('#app')
@Tajcore
Copy link

Tajcore commented Feb 7, 2024

getting Uncaught (in promise) TypeError: Cannot read from private field from this.observer.setOptions

@wobsoriano
Copy link
Author

wobsoriano commented Feb 8, 2024

@Tajcore Looks like a Proxy bug somewhere. You can add toRaw to the observer property and the error is gone.

watch: {
  todoId(id) {
    toRaw(this.observer)?.setOptions({
      queryKey: ['todo', id],
      queryFn: () => this.fetchTodo(id),
    })
  }
}

@Tajcore
Copy link

Tajcore commented Feb 8, 2024

Oh word I'm actually writing this using a class based approach using vue-facing-decorator that doesn't support the dynamic nature of updating the queryKey sadly. would you update the gist though just asking?

Thanks seeing it now!

@Tajcore
Copy link

Tajcore commented Feb 26, 2024

Hey @wobsoriano, how would you handle mutations? with just query-core

@JSFTI
Copy link

JSFTI commented Jan 6, 2025

@Tajcore It is very late, but I found out that tanstack/vue-query integrates just fine in Options API. I am using [email protected].

Follow the installation instruction. Add these to the entry point.

app.createApp({});

import UserApp from './user/UserApp.vue';

app.component('UserApp', UserApp);

// ...

+ import { VueQueryPlugin } from '@tanstack/vue-query'
+ app.use(VueQueryPlugin)

app.mount('#app');

Note that my setup is a little unconventional because my index.html entry point is like this. For context, my tech stack is Laravel + Vue. The important part is UserApp component.

<div id="app">
  <user-app :constant="{{ json_encode(array_merge($constant)) }}" />
</div>

In UserApp, provide the queryClient. I tried to use useQueryClient directly in data, but it throws an error. Something about the hook needs to be called in a setup() environment or has a provide context.

<script>
import { useQueryClient } from '@tanstack/vue-query';

export default {
  provide(){
    return {
      queryClient: useQueryClient(),
    }
  },
  // ...
}
</script>

<template>
  <!-- Content -->
</template>

If the project uses mixins, placing the inject in the mixins configuration might provide some shortcuts in further development.

export default {
  inject: ['queryClient'],
  // ...
}

Alternatively, you can just add a single setup() function in Options API. However, you need to do this in every component that needs access to query client.

<script>
// Alternative method, implicitly use useQueryClient only when needed.
import { useQueryClient } from '@tanstack/vue-query';

export default {
  setup(){
    return {
      queryClient: useQueryClient(),
    };
  }
  // ...
}
</script>

<template>
  <!-- Content -->
</template>

Now, for useQuery. It is possible to just use it directly in data. It will still be reactive, though with some caviat. A reactive queryKey must be wrapped in reactive or ref.

<script>
// Alternative method, implicitly use useQueryClient only when needed.
import { useQueryClient } from '@tanstack/vue-query';

export default {
  data(){
    const filter = reactive({
      search: '',
    });

    return {
      filter,
      query: useQuery({
        queryKey: ['items', filter]
      }),
    };
  }
}
</script>

<template>
  <!-- Content -->
</template>

Any changes to filter will be reactive. The query will refetch every time filter is modified.

Mutations are similar.

<script>
// Alternative method, implicitly use useQueryClient only when needed.
import { useQueryClient } from '@tanstack/vue-query';

export default {
  data(){
    const filter = reactive({
      search: '',
    });

    return {
      filter,
      query: useQuery({
        queryKey: ['items', filter],
        queryFn: () => { /* ... */ },
      }),
      mutate: useMutation({
        mutationFn: this.submit,
        onSuccess: this.success,
      }),
    };
  },
  methods: {
    submit(){
     // ...
    },
    success(){
     // ...
    },
  }
}
</script>

<template>
  <!-- Content -->
</template>

I created an example CodeSandbox . Currently, I am adding another example inspired by TkDodo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment