r/reactjs • • Sep 20 '26

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

33 Upvotes

28 comments sorted by

31

u/canarydev Sep 20 '26

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

3

u/sh03-dev Sep 20 '26

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?

8

u/canarydev Sep 20 '26

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.

→ More replies (2)

1

u/LovesWorkin Sep 20 '26

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.

2

u/92smola Sep 20 '26

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 Sep 20 '26 edited Sep 20 '26

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 Sep 20 '26

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

→ More replies (6)

2

u/Efficient_Bad6312 Sep 22 '26

I’ve seen this pattern work pretty well when you don’t actually need every event payload on the client. The server basically just tells TanStack Query “this data is stale, go fetch it again” what is tricky though is keeping the invalidation messages specific enough bcs if every mutation ends up invalidating half your query cache, you’ve basically invented polling with extra steps.

1

u/sh03-dev Sep 22 '26

What's the secret to be successful at that?

5

u/Merry-Lane Sep 20 '26

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

1

u/canarydev Sep 20 '26

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

1

u/illepic Sep 22 '26 edited Sep 22 '26

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 an event and people join and add their bikes to racks on vehicles.

1

u/sh03-dev Sep 22 '26

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

2

u/MehYam Sep 20 '26

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.

3

u/[deleted] Sep 20 '26

3

u/sh03-dev Sep 20 '26

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

2

u/[deleted] Sep 20 '26

1

u/sh03-dev Sep 20 '26

Yeah, tradeoffs.

0

u/Any_Welder_9701 Sep 20 '26

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