Creating domain objects
What would be the correct pattern to create domain objects that need multiple API requests?
In ngrx you would create a selector like this
This can be much more complicated as you can merge foreign objects into a user that are much deeper nested than in this example.
A post can have a property
likedByUsers and you can also merge that into the selector too.
What would be a tanstack query setup?41 Replies
stormy-gold•3mo ago
Do you really need to merge it into a user object though?
I remember a React project years ago with a very granular (and annoying) backend api as the micro service architecture wasn’t abstracted away from the frontend. First I needed to get an id then for other fields and relations other dependent queries.
I just kept all of these queries separate, where needed as simple dependent queries and relied on request deduplication and prefetching. There were data grids where each page could have a lot of queries - a single column could have many instances of the same query to fetch a name field for an id for example. Because of request deduplication that doesn't matter. Even though the backend was quite sluggish the frontend felt very snappy. Only merged such data in forms sometimes.
If data from multiple requests needs to be merged in a single object I'd definitely use RxJS to compose it into a single observable and pass that to queryFn. Note that this approach will result in a single cache entry for the combined data.
Dependent Queries | TanStack Query Angular Docs
injectQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it i...
stormy-gold•3mo ago
BTW - in the Angular community there's still a strong sense of a need to separate "smart" and "dumb" components. It depends, but I don't necessarily agree with that being always the best pattern. It can make code substantially more complex.
deep-jadeOP•3mo ago
Thanks a lot for your answer.
I have a few remarks. I try to keep it short and concise.
1. Creating one domain object enables us to create the object once and then pass it down to other components that then can work with it. It sure has downsides, e.g. less granular change detection.
2. Let's assume we have a
User and the user has Posts (nested relations can go much deeper as you already stated).
Keeping two separate objects for User and Posts_for_user would make the setup simpler as it doesn't require merging, I agree.
Do you "cross invalidate" queries from UserService to PostService and vice versa? Or does every mutation inside a Domain service invalidate their "own" queries?
3. Caching the combined query (using rxjs) under one cache key seems a bit impractical as I would always invalidate the entire object and refetch every entity in the merged object.stormy-gold•3mo ago
1. Is classic container components and why it often complicates applications. Everything needs to pass and be mapped through a container component. In my opinion it's outdated but often "not done" to say that. Unless someone requires you to follow that pattern you could consider if just getting whatever data you need in a component would simplify the architecture (within reason - a simple component can get some data from a parent. You can still organize queries in services of course - I'd recommend queryOptions for that.
2. If you structure the query keys well it's very easy to invalidate the queries needed. For example an array with a user id. Just invalidate everything with that user id. It's usually better to invalidate too much than too little.
3. Exactly: it's often much better to have separated queries. But container components make that hard and you may quickly need a ton of data mapping and merging code.
If it helps: having a query in a component is just injecting a data dependency. Even more when queryOptions keeps the fetching logic outside of the component. The component just declares that it needs the data and the service with QueryOptions and TanStack Query automate the whole fetching process.
deep-jadeOP•3mo ago
Ok, I can see what you mean.
Still, though. I can't get my head wrapped around the "cross service" invalidation of queries. That seems very confusing.
I can have many queries that are affected by this mutation. E.g.
stormy-gold•3mo ago
I don't know your application requirements but I can imagine it's all related to that user?
You could invalidate at user level, post level etc Structure the query keys according to those needs
So if a user has blogs posts and comments below that blog post, invalidating the user could invalidate all of those and cause a refresh of the data if all of these queries contain the user id in their key
deep-jadeOP•3mo ago
Yes,
user can have many relations in this example. If I understood correctly every relation of that user gets its own query (with api call), so it needs to be invalidated by the user service somehow, when it updates
ok, that means, in that user service we need to know all the other queries that might be affected by the update of the user, right?stormy-gold•3mo ago
Those queries probably already require a user id? So then it's natural to include it in the query key too
Even if the backend API doesn't require it you can still include it in the query key
deep-jadeOP•3mo ago
Yes, true.
stormy-gold•3mo ago
Just add some information to be able to make a distinction between coworker and post
queryKey: [COWORKER_KEY, userId()], queryKey: [POST_KEY, userId()], It looks like these could be the same now so you cannot invalidate just posts or just coworkers and if a coworker and a post would have the same id they would overwrite each others statedeep-jadeOP•3mo ago
Those queries need separate api requests so I thought they should get their own query. They both rely on a user id though.
stormy-gold•3mo ago
Yes they can have their own query but it looks like they share a query key
E.g. if you would have both a post and a coworker with id 1
deep-jadeOP•3mo ago
Yes, true. What would the invalidateQueries call look like?
stormy-gold•3mo ago
Your code example looks correct but consider the queryKey structure
deep-jadeOP•3mo ago
I'm not sure what you mean. Should I use a different key at index 0?
stormy-gold•3mo ago
I don't know what goes into COWORKER_KEY. If it's a number you have overlapping query keys between coworker and post
deep-jadeOP•3mo ago
Ah,
COWORKER_KEY = "coworker"; POST_KEY = "post"
Just an identifier for the entitystormy-gold•3mo ago
Right, yeah that would work
But you'll also need the actual id for post or coworker right?
deep-jadeOP•3mo ago
In this case I need the id of the user.
So both
updateUser and updatePost should trigger a refetch of
stormy-gold•3mo ago
I'd reverse it though: first userId then post
Then you can do:
this.queryClient.invalidateQueries({ queryKey: [userId() ] })
Both coworkers and posts would be invalidateddeep-jadeOP•3mo ago
Correct, using UUID that works. Then all queries that depend on
userId() need to start their queryKey with userId()stormy-gold•3mo ago
Yes easy peasy and far less code than forcing everything into a single object and a container component
deep-jadeOP•3mo ago
What about updating that post?
The query key of that post query doesn't include the
ID of the post as we don't have it upfront
If the post with id 123-abc gets updated I need to invalidate the query.
stormy-gold•3mo ago
It's multiple posts
deep-jadeOP•3mo ago
true, bad example
stormy-gold•3mo ago
Something like this?
queryKey: ['users', userId, 'posts', postId],
queryKey: ['users', userId, 'manager', managerId],
deep-jadeOP•3mo ago
There's no way to invalidate the query by the ID of the fetched entity right?
stormy-gold•3mo ago
Can you give an example? I think it's possible as you can include objects in a key
deep-jadeOP•3mo ago
stormy-gold•3mo ago
queryKey: ['users', userId, 'posts', postId, 'comments', commentId]
A simple array already allows a lot of control. With the example above you can invalidate everything for a user including the user data itself, their posts and comments, just their posts and comments on posts or just comments on their posts
deep-jadeOP•3mo ago
Inside this mutation
I only know that I'm updating a company and the companyId. So I can only invalidate all companies or the one id
stormy-gold•3mo ago
You need to reverse the key order in the array
You can also use objects but it might be a bit more complex. I've definitely seen code where it makes sense to use objects in keys though.
deep-jadeOP•3mo ago
But I can't update the query key after requests is finished right?
I'd need to do
stormy-gold•3mo ago
https://tkdodo.eu/blog/effective-react-query-keys You can find almost anything on Dominik his blog. The principles are usually easy to translate to Angular as I kept the API close to the original React.
Effective React Query Keys
Learn how to structure React Query Keys effectively as your App grows
deep-jadeOP•3mo ago
thank you
stormy-gold•3mo ago
I'm not sure what the purpose of that code is?
Generally rely on reactivity, both TanStack Query and signals are great for that
Exception is something like a router guard and even that may change with new signal based APIs in Angular
deep-jadeOP•3mo ago
I'm not sure how to invalidate a query by the id of the fetched entity.
This query can only be invalidated by the string "company" or
userId()
How do you invalidate the query in a mutation that updates the company by the companyId?
This only invalidates by companyId()stormy-gold•3mo ago
The ID of the fetched entity is usually on the right. If it's included in an object it's also possible to invalidate on that
fetchCompanyByUserIdAsync in that case the entity is the company so its id needs to be on the right
Or even without company id: [userId(), 'company']
But if you have an ID it's good to include it
[userId(), 'company', companyId()]
Or [userId(), 'companies', companyId()]deep-jadeOP•3mo ago
the problem is I only have the companyId after I get the response from the queryFn
stormy-gold•3mo ago
If a user has a single company [userId(), 'company'] is fine
deep-jadeOP•3mo ago
When I make mutation that updates the company using the companyId as input param I can't invalidate the query by the userId in the
onSuccess of the mutation as I don't have the userId in the mutation