logaretm / villus

🏎 A tiny and fast GraphQL client for Vue.js

Home Page:https://villus.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to share query state using hooks?

depsimon opened this issue · comments

What is the best way to share a query state across multiple components using hooks?

My use case is to have a useProduct hook that encapsulates the logic and use it in multiple components.
Here's how I'd like it to work.

// In `views/product.vue`
const { data: productQuery, execute: fetchProduct } = useProduct({ id: route.params.productId })
await fetchProduct()
// In `views/product/comments.vue`
const { data: productQuery } = useProduct({ id: route.params.productId })

// productQuery is loaded from the parent view, no need to execute the query again.
// In `views/product/edit.vue`
const { data: productQuery, execute: fetchProduct } = useProduct({ id: route.params.productId })

const onSubmit = () => {
  // submit edit form
  // re-fetch product, will update state in the hook and thus in `views/product.vue`
  fetchProduct()
})

Here's an attempt to such a hook

import { useQuery } from 'villus';
import { useRoute } from 'vue-router';

import {
  type ProductFindQuery,
  type ProductFindQueryVariables,
  ProductFind,
} from '@/graphql/schema';
import { NotFoundHttpError } from '@/utils/errors';

const { data, error, isDone, isFetching, execute } = useQuery<
  ProductFindQuery,
  ProductFindQueryVariables
>({
  query: ProductFind,
  cachePolicy: 'network-only',
  fetchOnMount: false,
});

export const useServiceProduct = async () => {
  const route = useRoute();

  if (data.value && !!data.value?.productById) {
    throw new NotFoundHttpError('This product does not exist', {
      backTo: { name: 'ProductList' },
      title: 'Product not found',
    });
  }

  return {
    data,
    error,
    isDone,
    isFetching,
    execute,
  };
};

But this triggers the "Cannot detect villus Client, did you forget to call useClient?" warning because useQuery is used outside the hook function.

What's the best way to implement this? I'd like to avoid using Pinia

Hello, sorry it took a while to get a look at this.

You can take advantage of provide/inject API here.

a quick implementation of this would look like this:

const PRODUCT_INJECTION_KEY = `PRODUCT_CONTEXT`;
// or this if you want to inject multiple instances at the same time
// const PRODUCT_INJECTION_KEY = (id) => `product_fetched_${id}`;

function useProduct({ id }) {
  const existingCtx = inject(PRODUCT_INJECTION_KEY, null);
  if (existingCtx) {
    return existingCtx;
  }

  // initialization logic for `useQuery`
  const queryCtx = useQuery(...);
  // ...
  provide(PRODUCT_INJECTION_KEY, queryCtx);
  
  return queryCtx;
}

I'm using this in production and it works quite well, let me know if there are still difficulties with this. I will try to add a similar example to the docs.

This is exactly what I needed it seems. Thanks for your time :)