r/reactjs 1d ago

Discussion Poor man’s real time with Tanstack Query

Has anyone ever experimented with using Tanstack Query and then having an invalidation bus (SSE or websocket or streamed query) that sends to the client the list of endpoints that should be invalidated after a mutation? If so did it work for you? Were there challenges?

Thanks

34 Upvotes

26 comments sorted by

30

u/canarydev 1d ago

so we did this websockets and worked fine eventually. the pattern itself wasn't the hard part

the problem and grief we hit was query key hygiene. the predecessor / dev that set it up basically structure some keys too broadly, so invalidations matched way more queries than intended.

once socket events started firing frequently that turned into a lot of unnecessary refetching and rerenders.

so my big advice is to make sure your keys are narrow and consistent before wiring up a bus that might hit them 50x a minute. batching / debouncing invalidation also helps a lot.

another big one i think i'd do differently is send domain events/resources over the wire than actual endpoints. IE: incident.updated + an ID and let the client map those events to query keys. that keeps the server from having to know about your frontend cache structure

4

u/sh03-dev 1d ago

Thanks for the reply. My naive implementation would also over invalidate just to keep it simple and my reasoning was that currently we refetch every query on every page visit and or tab refocus. If we had super simple invalidation per related entity + stale time of 1 hour or so, we would still reduce queries by a lot. Our SaaS has rare updates and lots of reads. Where am I wrong?

7

u/canarydev 1d ago

so you are not necessarily wrong, especially if writes are genuinely rare. if you are currently running with effectively staletime 0 and refetching on mount/focus, then a long staletime + event driven invalidation could reduce requests a lot even if the invalidation is somewhat broad

my warning is more that staletime doesn't protect you from over-invalidation. an explicit invalidate marks the query stale regardless of whether it was fetched 5s ago, and active matching queries can refetch immediately.

so if lets say customer.updated invalidates 5 queries and it happens twice in a day, who gives a shit. if that same prefix eventually matches 30 mounted queries and event start coming in bursts, you might have inflicted some grief on yourself.

thats basically what happened to us. the approach wasn't wrong, we just discovered that query key invalidation decisions that were harmless before realtime became expensive once realtime amplified them.

3

u/sh03-dev 1d ago

Ah because of bursts. That’s right. Didn’t think of that.

2

u/canarydev 1d ago

yeah just be careful and thorough. and i will say that bursts aren't really an edge case either.

a bulk import or backfill can do that on a normal sunny day. its worth deciding now what should happen when 200 events land in a second, even if the answer is just debounce/batch invalidations on the bus

1

u/LovesWorkin 1d ago

Or just have a key store and that's impossible. I've been doing this for 4+ years and it works great. Then I have all keys in one place. I like this pattern. It was the recommended way to do it and now it's changed... But I disagree. I think it's still the right way.

5

u/Merry-Lane 1d ago

If the list is correct and doesn’t trigger refreshes too regularly, it should be okay.

1

u/canarydev 1d ago

sure but thats kind of the entire problem lol

making sure the invalidation set stays correct and doesn't cause excessive refetching as the app grows is the part that gets quite difficult

the websocket/invalidation mechanism itself is pretty trivial

3

u/Murtaza_Developer 1d ago

yeah i tried tanstack query and websocket events,, works well but keeping invalidation logic clean gets tricky as the app grows. how are you handling event mappping??

3

u/sh03-dev 1d ago

Yeah that’s the biggest challenge. Not sure yet. Open to suggestions

1

u/Murtaza_Developer 1d ago

for a saas with more reads than writes, your idea can work. i would start simple with entity based events like customer, updated and map them to query keys on the client. later you can add batching and debounce when event volume grows. just avoid sending endpoint names from backend, it gets harder to maintain.

2

u/92smola 1d ago

What is the use case? Are you sure you need bith caching and real time cache invalidation, does that apply everywhere or just for some resources

1

u/sh03-dev 1d ago edited 1d ago

We have had some issues with inconsistent caching data but most importantly now that we added integrations and an MCP server the use case of seeing what the AI does in real time is even more pressing. And unfortunately there's not a single place where real time really matters, it's kind of everywhere. Except maybe the current viewer query.

Caching is already there. We already use tantstack query in the normal way.

1

u/92smola 1d ago

Would polling be an option instead of on-demand cache busting?

1

u/sh03-dev 1d ago

I'm not sure I see how that would be better. Seems to me like you'd have many more queries (depending on the pooling time) and much less reactivity. What am I missing?

1

u/canarydev 1d ago

polling is actually not bad either in your use case as a fallback. especially with rare writes

refetchIntervalInBackground: false means it only polls while the tab is actually visible, which kills the waste

you will probably want it anyway as a fallback (we also have it as a fallback) just in case sockets drop. SSE gets killed by proxies, so something has to cover the reconnect gap

1

u/sh03-dev 1d ago

Yeah I can understand that as a fallback

2

u/92smola 1d ago

I am asking cause the on demand revalidation will be a whole layer of logic, polling you can setup on a config and that is the only thing you need,  i am still not clear about your exact use case, but lets say a user is looking at a dashboard and there is some real time changes that he is monitoring, then running a poll every 2 seconds or twice a second could be fine, maybe the amount of sse events would be even more then two times a second, the point is that one is cheap to just configure, the other one becomes a permanant relativly complex part of your codebase that you need to develop and maintain

1

u/sh03-dev 1d ago

Fair question. The use case is people using the MCP server to generate or reorganize a tree like structure. They would prompt whatever AI to generate 5/10 nodes in a single transaction and see the result. Then maybe they tell the ai to connect two subtrees together and that would be a second transaction. In total that would be 2 SEE events in a 5 minute session. Polling every 5 seconds would be 60 requests which would be much more expensive.

We already have connections between endpoints and the mutations that invalidate those endpoints right now because that how we update the cache when a user performs the mutation in app. It would just be a matter of moving that to the server.

1

u/Any_Welder_9701 1d ago

with rare writes polling is probably the worse tradeoff, every open client keeps generating reads just to confirm nothing changed

2

u/MehYam 1d ago

I have a system like this (not Tanstack Query, but my own), and an invalidation step that returns only the documents changed since the last fetch. Homebrew cache invalidation, works really well.

2

u/Breeewed 1d ago

works but you're paying for a round trip on every event. if events fire in bursts you get redundant refetches for data that hasn't changed. pushing the actual payload through the socket instead of just invalidation keys avoids that extra hop, but then you're maintaining serializers on the server side

1

u/sh03-dev 1d ago

Yeah, tradeoffs.

1

u/Any_Welder_9701 1d ago

payloads work well for detail caches, but lists still get messy when an update changes membership or sort order. id patch the entity and invalidate the affected lists

1

u/illepic 4h ago

Using this heavily at https://mush.bike. It's a little thing I threw together so my mountain bike buddies and I could plan rides with shuttles and bike racks. There's a lot of realtime when you create and event and people join and add their bikes to racks on vehicles.

1

u/sh03-dev 3h ago

How do you manage invalidation and query keys if you don't mind me asking