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: overwrite query key on trpc.useQuery options #4989

Open
1 task done
zirkelc opened this issue Nov 2, 2023 · 9 comments
Open
1 task done

feat: overwrite query key on trpc.useQuery options #4989

zirkelc opened this issue Nov 2, 2023 · 9 comments
Labels
✅ accepted-PRs-welcome Feature proposal is accepted and ready to work on

Comments

@zirkelc
Copy link

zirkelc commented Nov 2, 2023

Describe the feature you'd like to request

I have query procedure listPosts which accepts some generic parameters and and optional lastUpdatedAt timestamp.
The query key looks something like this:

[["posts","list"],{"input":{ "parameters": {} },"type":"query"}]

This query is expensive, so I cache it also on the server with the given parameters as caching key in case multiple clients request the same data. When I receive the same parameters on the server, I return the data from the cache.

Now I need some way for clients to force a refresh on the server returning new data instead of cached data for the same parameters. So in this case, I query the listPosts with an optional lastUpdatedAt timestamp. The query key is now:

[["posts","list"],{"input":{ "parameters": {}, "lastUpdatedAt": 123456789 },"type":"query"}]

The servers receives the same parameters as before and finds the data in the cache, but the lastUpdatedAt timestamp is greater than the cached timestamp. So it runs the expensive operation on the server and returns the new data (and caches it).

Now I have two query keys on the client which are logically the same. I must update the query data of key 1 and key 2 with new data, because query key 2 is only temporary to trigger a server cache-invalidation. The lastUpdatedAt that I sent in query key 2 is a local state that will get lost after page navigation. When I later get back to the ListPosts page, it will use query key 1 again with data which was maybe set by query key 2.

I'm currently using onSuccess of useQuery to set the query data of all query keys matching the input without lastUpdatedAt:

type Input = {
  parameters: Record<string, any>;
  lastUpdatedAt?: number;
};

const query = trpc.posts.list.useQuery(input, {
  onSuccess(data) {
    const { lastUpdatedAt, ...restInput } = input;
    // update the query cache for all entries with the same parameters, but ignoring the lastUpdatedAt date
    queryClient.setQueriesData(getQueryKey(trpc.posts.list, restInput), data);
  },
});

That means it sets the data for query key 1 (without lastUpdatedAt) and 2 (with lastUpdatedAt).


However, onSuccess was deprecated in React Query v5, so I'm looking for an alternative option.
Allowing to overwrite the generated tRPC query key would make it possible to send different inputs to the server but with the same query key.

Describe the solution you'd like to see

I would like to overwrite the query key via the tRPC query options:

const query = trpc.posts.list.useQuery(input, {
  trpc: {
    // static query key overwrite
    queryKey: [["posts","list"],{"input":{ "parameters": {} },"type":"query"}],
    // OR
    // dynamic query key overwrite
    queryKey: () => {
      // remove field that should not be passed to server
      const { lastUpdatedAt, ...restInput } = input;
      return getQueryKey(trpc.posts.list, restInput)
    },
  },
});

That means the following two inputs produce the same query key:

// = query key: [["posts","list"],{"input":{ "parameters": {} },"type":"query"}]
const input1 = {
  parameters: {},
}

// = query key: [["posts","list"],{"input":{ "parameters": {} },"type":"query"}]
const input2 = {
  parameters: {},
  lastUpdatedAt: 123456789
}

By default, React Query would return the data from the query cache for both inputs. So we would need to use the refetch() function to manually trigger a refetch to the server with new input.

Describe alternate solutions

  1. useQuery.onSuccess() was deprecated in v5, but could be implemented again with a useEffect() with dependency to useQuery.data
  const { data } = trpc.posts.list.useQuery(input);

  // update all queries every time the data changes
  React.useEffect(() => {
    const { lastUpdatedAt, ...restInput } = input;
    queryClient.setQueriesData(getQueryKey(trpc.posts.list, restInput), data);
  }, [data, queryClient, input]);
  1. use useQuery directly with the proxy client and change the query key
const { lastUpdatedAt, ...restInput } = input;
const queryKey = getQueryKey(trpc.posts.list, restInput);

const query = useQuery({
  queryKey,
  queryFn: (context) => {
    return utils.client.posts.list.query(input)
  },
});

Additional information

No response

👨‍👧‍👦 Contributing

  • 🙋‍♂️ Yes, I'd be down to file a PR implementing this feature!

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar
@geoffreydhuyvetters
Copy link

Been bumping into the same situation, where I'm fetching things based on coordinates. But don't need to store all of the data in the cache

@KATT KATT added the ✅ accepted-PRs-welcome Feature proposal is accepted and ready to work on label Nov 9, 2023
@danielsyang
Copy link

I'm really interested in this feature/bug and would love to contribute, will take a look!

@danielsyang
Copy link

danielsyang commented Jan 10, 2024

Based, on the two alternative solutions, I believe adding a new key queryKey will be the simplest solution, agreed @KATT ?
But at the same time I wonder if this is a React-Query issue instead

@dkrieger
Copy link

API-wise, I think something like withQueryKey that takes a function, where x => x is the default, that allows you to modify the query key that trpc generates (x in my example), would be ideal

@Nick-Lucas
Copy link
Contributor

Nick-Lucas commented Jan 22, 2024

So far this looks a lot more complex to use than

const query = useQuery()

useEffect(() => {
  query.refetch()
}, [lastUpdatedAt])

I don't personally think we should be considering features that can be easily and more simply implemented in userland

Modifying the query key in place also has the impact that using the query in two different places without the additions could result in the same data landing in your cache twice and only one copy getting updated

@dkrieger
Copy link

Hard disagree. The abstraction over cache keys is leaking. Utilizing other hooks to deal with the missing feature is not cleaner calling code by any stretch.

@hichemfantar
Copy link

I'm all for finding a clean way to solve this.

API-wise, I think something like withQueryKey that takes a function, where x => x is the default, that allows you to modify the query key that trpc generates (x in my example), would be ideal

This approach seems like a good way to override the qk

@hichemfantar
Copy link

So far this looks a lot more complex to use than

const query = useQuery()

useEffect(() => {
  query.refetch()
}, [lastUpdatedAt])

I don't personally think we should be considering features that can be easily and more simply implemented in userland

Modifying the query key in place also has the impact that using the query in two different places without the additions could result in the same data landing in your cache twice and only one copy getting updated

What you mentioned makes sense but it'd be great if the qk was open for modification. This wouldn't be done very often but it's very convenient to have this escape hatch when we need it.

@hichemfantar
Copy link

hichemfantar commented Mar 12, 2024

@Nick-Lucas this is one of the biggest reasons for overriding the query key.
https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries#lazy-queries

function Todos() {
  const [filter, setFilter] = React.useState('')

  const { data } = useQuery({
      queryKey: ['todos', filter],
      queryFn: () => fetchTodos(filter),
      // ⬇️ disabled as long as the filter is empty
      enabled: !!filter
  })

  return (
      <div>
        // 🚀 applying the filter will enable and execute the query
        <FiltersForm onApply={setFilter} />
        {data && <TodosTable data={data}} />
      </div>
  )
}

edit: trpc seems to handle the enabled prop properly so modifying the qk directly isn't necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✅ accepted-PRs-welcome Feature proposal is accepted and ready to work on
Projects
None yet
Development

No branches or pull requests

7 participants