vercel / swr

React Hooks for Data Fetching
https://swr.vercel.app
MIT License
30.21k stars 1.21k forks source link

Initial fallback for request with dynamic urls #1520

Open macsikora opened 2 years ago

macsikora commented 2 years ago

Bug report

Description / Observed Behavior

I have functionality in the app which includes list with loading during scrolling + list filters.I am using useSWRInfinite with fallbackData option. The problem is that setting fallbackData makes data property from useSWRInfinite always set and in result between filter changes I see old data from fallback and no loading. It's replacing the list for a moment with original fallback list.

The problem is also that fallback option suggested in docs is object literal, where swr allows keys others than string, these keys are stringified by internal functions of swr (serialize). So in result where I have complicated server query with many params I am not able to replicate the key in form of string because swr doesn't do simple JSON.stringify by uses internal serialize function.

Both those problems create no possibility to use prefetched data. The hack needs to be applied which checks if we have first call of useSWRInifinite. But this also do not solve all isssues as the page in the end is not in cache.

Here is example with standard useSWR https://codesandbox.io/s/useswr-fallback-behaviour-d9c8i

So yes I understand why it works like that - fallback is related with given key, so new key means fallback is applied. Problem is that I have one fallback for initial request, initial filters and this fallback does not apply to any other next filter. I understand why we cannot say it is fallback for initial request as useSWR doesn't see difference between one call and another outside of key.

I wanted to use fallback property but my key is serialised internally by swr and function for this is internal part of the lib. So I am stuck with that also. What I need to do is hack like this one:

https://codesandbox.io/s/useswr-fallback-behaviour-hacked-wrongly-6vbcf?file=/src/App.js But this does not work as useEffect is called right after and we really have state change and "loading" instead of prefetched data. To deal with it we need some kind of counter like:

https://codesandbox.io/s/useswr-fallback-behaviour-hacked-counter-clve0?file=/src/App.js Take a look now it does work as expected (almost) as there is one rendering with fallback when we firstly change the key (its a small second but even though kind of not wanted behaviour).

Finally the problem does not allow me to easily use useSWR with prefetched data. I cannot:

Why do you use array, use string as a key

I could do that, but the current fetcher works with array of query params and makes a call from it. Also this fetcher is not only used in one place, in result it means that such a change has a big impact:

This is problematic as most fetcher utilities allow on using them with separated url and separated query params. Current fallback forces manually query params concatenation.

And the last is - why useSWR accepts as a key any other thing than string if fallback doesn't support this. It looks like something is missed in the design.

Expected Behaviour

Possibility to use Map instead of object literals in fallback in order to not needing to serialize manually the key. This would allow to pass arrays in the same way we can do that as key in useSWR.

Maybe some way to define fallback for category or requests. A way to tag/categorize some group of keys, For example I would like to say useSWR(key, {category: 'users_list', fallbackForCategory: fallbackData} then fallback would be applied only for first request of this category.

Repro Steps / Code Example

The example issue ( fallback shows for every key change): CodeSandbox - issue example

Example hack: CodeSandbox - hack example

Additional Context

SWR 1.0.1

shuding commented 2 years ago

Hi, thanks for writing the description carefully! This is definitely something we need to refine in the future. However, currently you can use this to serialize array/object keys and then use it as the key of fallback:

import { unstable_serialize, SWRConfig } from 'swr'

<SWRConfig value={{ fallback: {
  [unstable_serialize(['array', 'key'])]: 'fallback_data'
}}}

It's not documented, but we will once it gets finalized.

shuding commented 2 years ago

Also could you elaborate more on the "category" idea? Is it similar to scopped/namespaced cache? Not sure if I'm following.

macsikora commented 2 years ago

Hey @shuding many tnx for fast answer. Will check, but probably this saves the day for me. In terms of the idea I am not familiar with scopped/namespaced cache but it looks like similar idea. What I meant is I can define which category/namespace I am using and for this namespace set fallback. So not for some specific key, but for the whole namespace. In other words - namespace is empty - fallback is applied for the first row, key does not matters. In that way I could be not bother in checking what exact key had ssr, I would just define this category and say - this is fallback for first request.

macsikora commented 2 years ago

Hey, unfortunately unstable_serialize does not solve the problem. There are two issues with it:

  1. Browser request key has prefix like - $req$$inf$arg$ which I would need to append manually
  2. Sending undefined is not possible as this is not JSON serializeble - what means we need to add $undefined manually

In result this two problems forces to concatenate unstable_serialize result with $req$$inf$arg$ and with eventual $undefined what makes the solution cumbersome and hacky. The only way I see now is to use string urls always for keys.

shuding commented 2 years ago

Hi! For useSWRInfinite, you need to use

import { unstable_serialize } from 'swr/infinite'

And then

fallback: {
  [unstable_serialize(getKey)]: [initialPageData]
}

Sending undefined is not possible as this is not JSON serializeble

If undefined is in the key array, it should be possible (please let me know if it doesn’t) because we’re not using standard JSON serialization.

macsikora commented 2 years ago

Thank you @shuding . That works. One question more as it is my concern now. Fallback works fine, but it is never used as real data, in other words it makes better UX, but when we look at it from technical stand point I have one request on the server side, and right after landing in the browser I have another request. Yes fallback is shown but still it is not considered as real data, only as placeholder. If we do that instead:

  useSWRConfig().cache.set(unstable_serialize(getKey), initialPageData);

We have a cake and can eat it too. So we fully use the data fetched from server as legit fetched one. And this exactly we want I suppose in most cases. If the server call is done right before rendering there is no sense in having another call right after, as it totally makes the initial fetching unnecessary.

Ok so question is why in nextjs docs we have info about using fallback, and why there is no info that we can set the cache directly. Maybe this is wrong approach, if so why?

Antonio-Laguna commented 2 years ago

@macsikora I have exactly the same use case and exactly the same question. Where are you putting useSWRConfig call ?

shuding commented 2 years ago

Sorry that I missed your previous comment @macsikora.

The reason that SWR doesn’t encourage writing to the cache is because that can be dangerous. Writing to the cache is a mutation, which can cause side effect or undefined behaviors (race conditions, unpredictable order and result, etc). However fallback won’t cause these side effects because there is no write behavior.

But you can still prefill the cache with these initial data, by doing it at initialization rather than explicitly write to it:

function App({ data }) {
  return (
    <SWRConfig value={{ provider: () => new Map(Object.entries(data)) }}>
      <Page/>
    </SWRConfig>
  )
}

https://swr.vercel.app/docs/advanced/cache#create-cache-provider

Antonio-Laguna commented 2 years ago

@shuding How does that relate to the serialised key though?

shuding commented 2 years ago

@Antonio-Laguna Not related, I was answering the question regarding writing to the cache verses serving as fallback.

shuding commented 2 years ago

We now have docs for cases like this now: https://swr.vercel.app/docs/with-nextjs#complex-keys

It’s marked as unstable because the API (wrapping with [] and then call a serialize function) is still not ideal, and we can probably find a better way to do this in the future. Any ideas are welcome!

Antonio-Laguna commented 2 years ago

@shuding the issue is that as @macsikora hints, I don't want to repeat a request for which I already have data provided as fallback and unless I'm missing something, it's unavoidable. The data is already rendered and already visible but a request is made nevertheless

That works. One question more as it is my concern now. Fallback works fine, but it is never used as real data, in other words it makes better UX, but when we look at it from technical stand point I have one request on the server side, and right after landing in the browser I have another request. Yes fallback is shown but still it is not considered as real data, only as placeholder.

shuding commented 2 years ago

@Antonio-Laguna With this approach, the extra request can be avoided if you have revalidateIfStale: false:

function App({ data }) {
  return (
    <SWRConfig value={{ provider: () => new Map(Object.entries(data)) }}>
      <Page/>
    </SWRConfig>
  )
}

By disabling that option, if the data exists (stale), and SWR will not revalidate when mounting.

However the revalidation on mount is still helpful for highly dynamic data, especially for SSG cases (getStaticProps), the data was fetched at built time and it might change when the webpage is actual visited.

Antonio-Laguna commented 2 years ago

@shuding thanks for getting back!

I don't think that works though. I think (could be wrong) you're proposing something for, let's imagine a "movie" page. In that case, the page is considered "static" as one URL matches one movie.

In my case, the page however is dynamic. It's a search page and has filters so only the initial combination should actually be avoided whereas if I change a filter value, I want SWR to kick in and fetch the data with the new value. I'm using useSWRImmutable and It's not SSG but SSR so it's all happening on getServerSideProps.

This is already happening except there's an extra request on mount which is what I want to avoid, it's not especially harmful but I don't need it since I know the data has just been fetched and it'll be extremely rare that I'd need it again.

Here's the rough code:

function MyComponent({ fallback }) {
  return (
    <SWRConfig value={{ fallback }}>
      <Page/>
    </SWRConfig>
  )
}

MyComponent.getServerSideProps = async function({ query, params }) {
  const searchArgs = [
    searchTerm,
    subtype,
    page,
    query.start_date,
    query.end_date,
  ];
  const dataKey = serializeSearchParameters( ...searchArgs );
  const fallback = {};
  fallback[ dataKey ] = await fetchData( ...searchArgs );

  return {
    fallback
  }
}
macsikora commented 2 years ago

Hey @Antonio-Laguna so what I am doing is that I generate the key by unstable_serialize and fetch the initial data in getServerSideProps and I pass it down, then I have a wrapper component, kind like:

const config = useSWRConfig();
  if (!isServer) {
    // set only client cache
    config.cache.set(props.swrMyKey, [props.myData]);
  }

Very important is to not do it in server side, as without the condition you will end in situation that you will have a cache data in ssr, that also means this thing will grow in memory of the server and can lead to bugs.

And isServer can be simple typeof window === 'undefined'. Also very important is what @shuding has mentioned, further using useSWR should have revalidateIfStale: false as without this it will still fetch it again.

fcandi commented 2 years ago

Hi! For useSWRInfinite, you need to use

import { unstable_serialize } from 'swr/infinite'

Thanks for the help. Its working fine, awesome library!!!!I

Andreas

P.S: I only wish the docs where up to date ;)