TkDodo / blog-comments

6 stars 1 forks source link

blog/effective-react-query-keys #25

Closed utterances-bot closed 2 years ago

utterances-bot commented 3 years ago

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

benjamhawk commented 3 years ago

Great blog. Can't wait to checkout the rest later!

cloud-walker commented 3 years ago

Very cool! QueryFilters totally answer my old question posted here! https://github.com/tannerlinsley/react-query/discussions/1640

Thanks!

wobsoriano commented 3 years ago

Ah, thanks for this.

Now time for some refactoring...

seblondono commented 3 years ago

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

huanguolin commented 3 years ago

Thanks! Inspired by this post, I write a utility method to build keys from config with good type hints😀: https://www.typescriptlang.org/play?#code/C4TwDgpgBAwghgZ2AHgBoBooE0B8UC8UqUEAHsBAHYAmC2UA-EVAFzYDcAUJ6JFAGKUCUABQA6CXABOAcwRs4lEAG0AugEoCeRStVce4aAGkIIGAHtKAMwCWM4QG9OUF1GUBrNkik3KM1WwmZpa29gA+AkIRAK6U7pTmAO6UXAC++rzQ8ADGABYQQQASEAA2kFIAKobIFSTkVLRQQRbWdpgACnUUNHTevjJqwmp4hE6ubu5QvlDupuZWUBUBix6qXQ104pKy8lA6apr42kpqzuOuTGPn1y7A5kFsIod4yhJi7Zjuemc353dBAHUbMBcgBBHaPN7SORsdrSOAAWwgFCkCBqqxwzzcbw+M0wbwASsjolJKFVIOivjhvr8XKkfuc2BVVusek1TC1QgzxpcoP9TI8sa8JLivlBUlAAGSwOB5AqmYplCCVarwJCU1SYZohOw4TBqlDC96fTVQPp+YY4bmuNgOPn3AWiIU4k3itL6MhgcxSYBQKyxbLAGyWKAAI2iNhK1AAsuZqNEShAcvkgjVWY1ta0ZDgRLNglmmZgwFIILZSF5gD4LWtCAc2Mn5SBFeVyRAangri5spYkDNTIJhCJKIiIBWqzIscXSzZSGJu5RsnBgCJlMOkRouON5727uYSgB5UMAK0cEsQ7PzoU3ris3tE299k3mUEPR4ggbEeYQuY5Oonmk7c4HygAA3YQ805OxVmva4bAWERMmfMD8BQqAAHJ-QXINLDQgDrWuXcD2PFlRnw2l+RAIEQXBGFRChCEoFieIkkoA4tGxCQ80EXN1HxCQQK2MRoQQdRNTI34KMFdiuMoHj0HE1x0jIiVSgQaA4NEMDfCQRRsggZ9X3fYA8NpVxCNfEioDtSSnWk-tZPcXioDecNIxjOMEyTWUU1MEQwPPTNQk+eyeM0JTfhUko1KshTbnMPcLLFUjTIIh0QCko4+xAbjHPklK3WUhl6XGEtgBJIRzOPPY6AbIpShbaoKhwNJuAAelavkICQThgISIM9MCuxHAZPqbD03ZAPGEobCQW1Yr2EoSkedwbGoABJag2EoaIEVDZVMFsEoUVtLLEm9TazUrfp2CmBB+DgEDvWBUcw3ixNFHFIUVvW6gDsjFExPyqwHqeihltWjatp2vapD+o7lROvMzqkC7zRkG6Zvux6fDB169wgD7Ui+iHfr9f7lUBm5Ujym5qGROBIzYWI6dsSgIF+or5KUzh2qgOnshKaRoEwwNgyEAAhCMo1jeNE1q+zWzTMhugzX8sxzedQiZdR628xtm2VRWmq4XngN9QhJbcmXPPl7KyUMIdzH6xtIInLgzeEVzpY8uW9aCR3ncGt2ep7X1gAARmEYAxFG8axAop53dDvkACYo5jp2xq6sRpqQeO0sTkPKB3ABmdPY+z3Po7gRb88BYEwR2ERw8wO0kfOtg0LQzBMZBnGXuBqLoCJpPi7DgAWcvM7jquxGB7Hnrr3z1FHncAFYp+dhAc5m6P59BiAl8ohuaO-FurNOjv0O726sYPthB+ikei53AA2Tes+3ungAZkoj8LoAA

ziyadkhalil commented 3 years ago

The query key factory idea is supreme. Thanks!

seanbruce commented 3 years ago

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 ?

TkDodo commented 3 years ago

@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 😅

JaeYeopHan commented 3 years ago

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:

TkDodo commented 3 years ago

@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

Justinohallo commented 3 years ago

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.

TkDodo commented 3 years ago

@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

dima-getselectstar commented 3 years ago

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:

Or:

In my case I need to fetch list of tables or particular table. So would you recommend to design it?

TkDodo commented 3 years ago

@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

katesroad commented 2 years ago

The factory idea is so brilliant for big projects.

tiagonevestia commented 2 years ago

Very cool!

nicoqh commented 2 years ago

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.

TkDodo commented 2 years ago

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.

georgeteo commented 2 years ago

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.  
TkDodo commented 2 years ago

@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.

aantipov commented 2 years ago

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.

TkDodo commented 2 years ago

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.

sandves commented 2 years ago

"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?

TkDodo commented 2 years ago

@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.

nazmeln commented 2 years ago

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?

Is it connected with the usage of useInfiniteQuery where I'm passing the params as object?

Smth like:

useInfiniteQuery(todoKeys.todos({idIn: 'ids'}))
TkDodo commented 2 years ago

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 :)

dharmil commented 2 years ago

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

TkDodo commented 2 years ago

@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.