Closed utterances-bot closed 2 years ago
Great blog. Can't wait to checkout the rest later!
Very cool! QueryFilters totally answer my old question posted here! https://github.com/tannerlinsley/react-query/discussions/1640
Thanks!
Ah, thanks for this.
Now time for some refactoring...
I like the factory idea a lot. Thanks for making such a nice description of your ideas, I think I will be using a similar approach now that me and my team are moving from Redux to React Query :D
The query key factory idea is supreme. Thanks!
Query key factory is definitely what I want to use in out next project. About Colocate query key, we used to co-locate query key to feature folder, but the duplicated key become a problem, so we move all query keys to global to prevent the issue. Have you even encountered that problem ?
@seanbruce if every key starts with the name of the feature, there shouldn't be any clashes unless you have features with the same name. It was rather a problem for us to have global query keys, because they can get quite large, and if you then copy-paste one line but don't change the query key prefix (it happens!), you'll get key duplications which are very hard to spot. I've been there and it took me hours to find 😅
thanks for awesome article :)
query key factory concept is very useful! how about using api baseUrl (with path parameter, query parameter) to query key? :eyes:
@JaeYeopHan yes, you can do that as well and then even leverage the defaultQueryFn as described here: https://react-query.tanstack.com/guides/default-query-function
What is the reason for using "as const" in the query key factory.
When using this with Typescript, I get the following error:
No overload matches this call. Overload 1 of 3, '(queryKey: QueryKey, options?: UseQueryOptions<unknown, unknown, unknown> | undefined): UseQueryResult<unknown, unknown>', gave the following error. Argument of type 'readonly [...string[], "list"]' is not assignable to parameter of type 'QueryKey'.
However, when I remove the "as const" it works correctly.
@Justinohallo Have a read here on const assertions: https://tkdodo.eu/blog/the-power-of-const-assertions
the issue you’re seeing looks like it has something to do with incorrectly typed options, have a look here: https://github.com/tannerlinsley/react-query/issues/2795
That's why I recommend one Query Key factory per feature ...
So, how should queries look in context of todoKeys
? Shall I have the following:
useTodos({queryKey: todoKey.all, onSuccess: () => {} })
useTodos({queryKey: todoKey.list, id, onSuccess: () => {} })
Or:
useTodos({onSuccess: () => {} })
useTodoList({onSuccess: () => {} })
In my case I need to fetch list of tables or particular table. So would you recommend to design it?
@dima-getselectstar I would do separate custom hooks, as I'm not a big fan of having a custom hook where consumers can pass in the query key. So the second solution would win for me
The factory idea is so brilliant for big projects.
Very cool!
The queries file will contain everything React Query related. I usually only export custom hooks, so the actual Query Functions as well as Query Keys will stay local.
What if updates in one feature requires invalidating queries from another feature?
Let's say a todo has a completedBy
field which contains a user (doesn't matter if the user is fetched from another endpoint, say /todos/1/user
, or is embedded in the todo response). Maybe this field is even used to convey whether the todo is completed or not.
If your query keys are local to to the feature, what if any todo queries need to be invalidated when a user is changed? Whether my example makes sense or not, I've had problems keeping these 100% separated in the past.
what if any todo queries need to be invalidated when a user is changed?
if features depend on each other, I think the best way is still to make the todo
query key factory "globally" available to all features that need to use it (user
in this case). Could be done by just moving it some place where both features can import it, or by having some sort of "orchestrator" that has access to all query key factories and passes the todo factory to the users feature.
Great job on react-query!
I had a question on whether it is possible to split out a batch request when inserting items into the cache.
For example, when using firebase, I have:
// returns a batch of data.
// Unfortunately, firebase fetches and bills for the whole document so no point in getting just the identifier.
const getBatch = (): Promise<Item[]>
// This caches against "items".
useQuery("items", getBatch)
// Later in the view, I will refetch the single document
const getSingle = (id): Promise<Item>
useQuery(["items", itemId], getSingle(itemId))
// If I could have expanded the items returned in getBatch and stored them by itemId, I could save a fetch here.
@georgeteo you can either use onSuccess
of the list query and fill the caches of each item query with queryClient.setQueryData
eagerly, or add initialData
to the item query and pull data from the list query with the initialData function.
Great article, as always! I have a question regarding the file structure you shared. You collocate query files with the "features"' files.
I think it works only if queries are local to the feature and are not used anywhere else.
In practice though, the same resource is needed in different places of a page and on different pages.
Wouldn't it be better in such case to make a shared folder for such queries?
At my work we have a "global" folder /queries/
and all our "features" use queries from there.
I prefer to colocate things as much as possible. In practice, there are many queries that will be used only in one feature, even if the feature itself is used on multiple pages. Of course, there are also truly global queries that are used across multiple features, which I would also put into a top level global directory. Alternatively, you can have one feature own the query and export it, but I have mixed feelings about this approach.
I have made the experience that some people prefer to put all queries to the top level immediately from the start, and I would disagree with that. It's the same reason why I don't like one global redux store for client state - because most global client state is still local to a feature / page. Using multiple zustand stores pays off here as well for example, especially if you scale up to multiple teams maintaining different features / pages in the same product.
"The queries file will contain everything React Query related". Does that mean that you also put mutations in queries.ts
or do you have a separate mutations.ts
that imports query keys from queries.ts
if needed?
@sandves On smaller projects, we've put everything in queries
. Now on even bigger projects we separate queries / mutations / keys in their own files. The important bit is that everything is located within the feature itself and not "accessible" outside of it.
I have such key:
const todoKeys = {
todos: (params?) => [{
type: 'list',
params
}]
};
Could someone tell me why I'm getting those values? Aren't those 2 keys are the same for the queryClient?
queryClient.getQueryData(todoKeys.todos(), {exact: false}); // undefined
queryClient.getQueryData(todoKeys.todos({}), {exact: false}) // real data
Is it connected with the usage of useInfiniteQuery where I'm passing the params as object?
Smth like:
useInfiniteQuery(todoKeys.todos({idIn: 'ids'}))
todoKeys.todos()
will expand to: [{type: 'list', params: undefined}]
. I don't think that you can have undefined
in your query key because it needs to be json serializable. Query Keys are hashed with JSON.stringify
. That being said, both keys are hashed, so I don't really know. I would try to not use undefined in keys though :)
Hi TkDoko,
Thanks for your excellent blog post. I am very glad that your articles are visible from React Query's documentation site, so that it can have a wider reach.
I've been trying to adopt your idea of query key factories and have been almost able to implement it successfully save for one thing: Typescript crying about mutable types.
In the the following example,
`const todoKeys = { all: ['todos'] as const, lists: () => [...todoKeys.all, 'list'] as const, list: (filters: string) => [...todoKeys.lists(), { filters }] as const, details: () => [...todoKeys.all, 'detail'] as const, detail: (id: number) => [...todoKeys.details(), id] as const, }
const test : unknown[] = todoKeys.lists();`
the return values of the factories as tuples can never be assigned to unknown[] (which is the type of the QueryKey parameter), as they contain readonly values.
The only way I know to get around this without losing the strong typing benefits of as const, is to either:
a. [...todoKeys.lists()] -> spread the array or tuple b. todoKeys.lists() as unknown as unknown[] -> Cast it to unknown[] with first being required to cast to unknown for that.
Option a certainly seems better -- both stylically as well as from a typing perspective. But the usage is unfortunately not so smooth as simply calling todoKeys.lists().
Do you have any suggestions?
Thanks, San
@dharmil I don't quite understand this:
the return values of the factories as tuples can never be assigned to unknown[] (which is the type of the QueryKey parameter), as they contain readonly values.
Here is a TypeScript playground example that shows how I'm using the keys. The tuples are assignable for the queryKey, because the QueryKey
itself doesn't have to be unknown[]
- it's a separate generic that has to extend readonly unknown[]
, and a tuple does suffice for that.
Effective React Query Keys | TkDodo's blog
Learn how to structure React Query Keys effectively as your App grows
https://tkdodo.eu/blog/effective-react-query-keys