psteinroe / supabase-cache-helpers

A collection of framework specific Cache utilities for working with Supabase.
https://supabase-cache-helpers.vercel.app
MIT License
499 stars 31 forks source link

Cache not updating after mutation when using fallback data in `useQuery` #502

Open nekusu opened 1 month ago

nekusu commented 1 month ago

Describe the bug
When using mutation hooks with fallback data set in the useQuery hook, the cache does not update after triggering a mutation. Specifically, after passing fallback data to useQuery via fetchQueryFallbackData, useUpdateMutation doesn't seem to update the cache as expected.

To Reproduce
Steps to reproduce the behavior:

  1. Implement useQuery with fallbackData as shown in the example below:

    // Server component
    const [, fallbackData] = await fetchQueryFallbackData(
      supabase.from('users').select('id,avatar_url,name').eq('id', id).single(),
    );
    
    // Client component
    const { data: user } = useQuery(
     supabase.from('users').select('id,avatar_url,name').eq('id', id).single(),
     { fallbackData },
    );
  2. Trigger a mutation using useUpdateMutation to update a user record:
    const { trigger: updateUser } = useUpdateMutation(supabase.from('users'), ['id']);
  3. Observe that the cache doesn't update after the mutation is triggered, even though it should.

Expected behavior
The cache should be updated after a mutation when using useQuery with fallbackData.

Additional context

nekusu commented 1 month ago

Additionally, the Supabase clients I'm using for both the server and the client are set up following this guide: https://supabase.com/docs/guides/auth/server-side/nextjs?queryGroups=router&router=app

psteinroe commented 1 month ago

did you try to pin-point the cause? cache updates are working in normal circumstances, and I wonder what causes it to break here. if revalidateIfStale: true causes a revalidation, then the cache item is hit correctly and the update is not being applied. can you show the full code, including how you call the updateUser mutation? does removing the fallbackData change anything?

nekusu commented 1 month ago

I’ve made some progress on the issue. Here’s what I found:

I noticed that when passing only the fallbackData directly to the useQuery hook, the cache was not populated correctly. The SWR cache would only contain the key without any data. This was confirmed by inspecting the cache with:

const { cache } = useSWRConfig();

When fallbackData is set, the value for the cache key doesn’t include the data, which causes mutations not to work properly. However, when I remove fallbackData, the cache correctly contains the data and updates as expected.

What worked:

Instead of passing just fallbackData to the useQuery hook, I used both the key and the data returned from fetchQueryFallbackData and passed them to SWRConfig (setting fallback instead of fallbackData in useQuery also works). This ensured that the cache was populated correctly and that mutations worked.

Hypothesis:

It seems that when passing only fallbackData, SWR treats it as a temporary value for the initial render, but it doesn’t associate the data with the cache key or persist it. As a result, the cache isn’t set properly and subsequent mutations fail to apply. By using the key generated by fetchQueryFallbackData, the cache is correctly associated with the data, allowing it to be updated by mutations. This issue arises specifically when revalidateIfStale is set to false, otherwise the cache would be populated on mount as normally.

I believe this should be corrected in the documentation. Currently, it mentions that you can either pass fallbackData directly to useSWR or define it globally in SWRConfig, but it seems that they don't behave the same way. The correct procedure should be set fallback on either the useQuery hook or globally in SWRConfig.

Let me know if you agree with the analysis, I can do a PR updating the docs!

nekusu commented 1 month ago

After further testing I realized that I was wrong about the solution in my previous comment. I was incorrectly setting the fallback in SWRConfig.

Initially I was passing it as [string, any] as returned by the fetchQueryFallbackData function instead of { [key: string]: any }, meaning that the fallback was being ignored, causing the cache to not have that initial key and value at all and triggering a revalidation on mount. Once I fixed that and set the fallback correctly, the behavior matched what I observed when using fallbackData directly in the hook.

This means that currently, neither approach correctly handles the fallback, setting fallback correctly in SWRConfig produces the same behavior as setting fallbackData directly in the hook, the key in the cache is generated but its value isn't populated with the data regardless of where fallback is set and mutations fail to apply.

psteinroe commented 3 weeks ago

thanks for the detailed investigation! Did you confirm the same behaviour with using plain swr? I am struggling to find the cause in cache helpers for this.

nekusu commented 3 weeks ago

I ended up using the react-query package. If I remember correctly, the root cause of the issue was that setting fallbackData in SWR does not prepopulate the cache. As a result, subsequent mutations fail because there is no existing cache data to mutate.

This behavior contrasts with react-query, where you have Initial Query Data and Placeholder Query Data. initialData is persisted to the cache, while placeholderData is not. In SWR, fallbackData behaves similarly to placeholderData, meaning it only serves as a temporary placeholder when data is undefined.

Found a comment that proves this: https://github.com/vercel/swr/issues/2114#issuecomment-1327657957