RTKQ Architecture ideas: research TanStack Query, other libs, coordination approaches
Jotting down some notes in the back of my head:
- We should probably review TanStack Query to see if there's any additional useful ideas we can glean in terms of architecture, feature set, implementation approaches, etc.
- Probably a bunch of other libraries too (SWR, RESTHooks even though it has no real downloads, etc?)
- The original design was entirely around individual hooks, but we've seen lots of users want to do queries in more coordinated ways: sequential, combined, etc. In other words, the next layer up from "run this one query". Currently this devolves into "just call the hooks and let your component coordinate things", but that's not always ergonomic. I know this is vague, but worth thinking through a bit.
- Similarly, we've grown a half-dozen different aspects of internal query coordination: hooks, thunks, polling, invalidation, lifecycles, delayed invalidations, etc. It's all been very ad-hoc and it feels like we're stretching the bounds of what we can do here. Worth thinking through the pieces and figuring out if there's any consolidation or better design.
- Obligatory "bundle sizes are too big and all the thunks and logic are complicated, can we redesign the internals to be nicer somehow?"
- would still like to do something around normalization, but maybe a TSDB adapter sufficiently addresses that?
and then I'm sure there's plenty of related ideas in #3692 .
One of the things I like about SWR is the ergonomics around mutate.
In the same realm of the discussion in #5073 of helpers around manual cache (mainly optimistic & pessimistic) updates, I think that having easy access to a bound update from a query hook would make a lot of use cases of simpler.
How this works for SWR, example adapted from their docs:
const { data, mutate } = useSWR('/api/user', fetcher)
const updateName = async () => {
const newName = data.name.toUpperCase()
await requestUpdateUsername(newName)
mutate({ ...data, name: newName })
}
Hmm. So essentially what we do for cache lifeycles?
updateCachedData: (isAnyQueryDefinition(endpointDefinition)
? (updateRecipe: Recipe<any>) =>
mwApi.dispatch(
api.util.updateQueryData(
endpointName as never,
originalArgs as never,
updateRecipe,
),
)
: undefined) as any,
Seems pretty doable to throw that into the query hooks.
Yeah-- it would be pretty sweet to be able to do:
// Optimistic patch example, within-consumer
const { data: profile, patch: patchProfile } = useGetProfileQuery(userId);
const [updateProfileMutation] = useUpdateProfileMutation();
const updateName = async (name: string) => {
const patch = patchProfile({ name });
try {
await updateProfileMutation(userId, { name });
catch (error) {
patch.undo();
}
}
I think one of the reasons I'm a fan of this approach is that in applications with lots of optimistic / pessimistic updates, it becomes nice for it be super obvious how the updates are occurring from just reading the consumer.
Especially when a variety of strategies are used for updating, and they all start feeling like lots of side effects firing off.
Sure, filed:
- https://github.com/reduxjs/redux-toolkit/issues/5151