"Atomic" cache invalidation for more UI stability #8863
Unanswered
antoinechalifour
asked this question in
Ideas
Replies: 2 comments
-
we’ve discussed this some more in discord. @antoinechalifour please post the solution for the |
Beta Was this translation helpful? Give feedback.
0 replies
-
Want to post a few of the solutions we came up in the discord here just for discoverability. Wonder if there good enough to put in docs as is. Option 1 - locked on first call, blocks invalidation till unlocked: const useSyncedMutation = () => {
const schedulerRef = useRef<'unlocked' | 'locked'>('unlocked');
const queryClient = useQueryClient();
const lockScheduler = () => {
schedulerRef.current = 'locked';
return () => {
schedulerRef.current = 'unlocked';
};
};
const synced = async (getPromise: () => Promise<unknown>) => {
if (schedulerRef.current === 'locked') {
return;
}
const unlock = lockScheduler();
const promise = getPromise();
return new Promise((resolve, reject) => {
notifyManager.setScheduler(async (cb) => {
try {
await promise;
resolve(undefined);
} catch (e) {
reject(e);
} finally {
cb();
unlock();
notifyManager.setScheduler(defaultScheduler);
}
});
});
};
return useMutation({
mutationFn: () => Promise.resolve(),
onSuccess: async () => {
await synced(async () => {
return Promise.allSettled([
queryClient.invalidateQueries({
queryKey: numbersQueryOptions().queryKey,
}),
queryClient.invalidateQueries({
queryKey: lettersQueryOptions().queryKey,
}),
]);
});
},
});
}; Option 2 - has a queue of invalidation promises, locks when first promise added, unlocks when empty: const useSyncedMutation = () => {
const schedulerRef = useRef<'unlocked' | 'locked'>('unlocked');
const promiseSetRef = useRef(new Set<Symbol>());
const resolveSchedulerUnlockRef = useRef<(_: unknown) => void | null>(null);
const rejectSchedulerLockRef = useRef<(_: unknown) => void | null>(null);
const queryClient = useQueryClient();
const lock = () => {
schedulerRef.current = 'locked';
let promise = new Promise((res, rej) => {
resolveSchedulerUnlockRef.current = res;
rejectSchedulerLockRef.current = rej;
});
notifyManager.setScheduler(async (cb) => {
await promise;
cb();
schedulerRef.current = 'unlocked';
resolveSchedulerUnlockRef.current = null;
rejectSchedulerLockRef.current = null;
notifyManager.setScheduler(defaultScheduler);
});
};
const synced = async (getPromise: () => Promise<unknown>) => {
if (schedulerRef.current === 'unlocked') {
lock();
}
const promiseId = Symbol('promise');
promiseSetRef.current.add(promiseId);
try {
await getPromise();
} catch (e) {
(rejectSchedulerLockRef.current as (_: unknown) => void)(e);
} finally {
promiseSetRef.current.delete(promiseId);
if (promiseSetRef.current.size === 0) {
(resolveSchedulerUnlockRef.current as (_: unknown) => void)(undefined);
}
}
};
return useMutation({
mutationFn: () => Promise.resolve(),
onSuccess: async () => {
await synced(async () => {
return Promise.allSettled([
queryClient.invalidateQueries({
queryKey: numbersQueryOptions().queryKey,
}),
queryClient.invalidateQueries({
queryKey: lettersQueryOptions().queryKey,
}),
]);
});
},
});
}; |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hi everyone!
I recently migrated parts of my Remix app from loaders + actions to React query. I'm pretty happy with the migration since I got more control of caching and I can invalidate only some cache entries instead of revalidating at the route level.
The only thing that I miss from the Remix times is the "stable" experience in some instances.
What do I mean by this ?
In a Remix (or RR7) app, when an action is called, the router calls all matching loaders + fetchers, collects all of their data, and commits the new server state at once. So you have prev state -> action -> new state.
After the migration, I am no longer using actions, but mutations, and am now invalidate queries myself.
But when I do things like this :
The app, in a few cases, is not as stable as it used to be since the 2 refetches in the background are not "synced" (there is no router commiting all fresh data at once)
I wonder if there is a way of replicating the very nice user experience I had on remix, having some "atomic" or "synced" way of triggering observers. I tried using the NotifyManager, but I don't think it's built for this. I thought of this API :
(There may be many tricky edge cases involved)
I started a discussion of the discord channel : https://discord.com/channels/719702312431386674/1348401703640105093/1348401703640105093
@DogPawHat suggested to update the cache, but not fire the observers.
I don't know what you all think of this idea. As a user it would be nice to opt-out of the "fire and forget" way of invalidating caches.
Thanks you all maintainers for the wonderful library, I've been loving it for years!
Beta Was this translation helpful? Give feedback.
All reactions