Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep previous result while revalidating #192

Closed
Svish opened this issue Dec 10, 2019 · 30 comments · Fixed by #1929
Closed

Keep previous result while revalidating #192

Svish opened this issue Dec 10, 2019 · 30 comments · Fixed by #1929
Labels
feature request New feature or request
Milestone

Comments

@Svish
Copy link
Contributor

Svish commented Dec 10, 2019

Is there an easy way, or if one could be added, to have useSWR keep the previous result while revalidating?

My case is a search field, and after I've gotten my first data, I'd really like the returned data to stick until a new request has completed (or failed). Currently the data goes back to undefined whenever the key changes.

@isBatak
Copy link

isBatak commented Dec 20, 2019

That means it's not stale xD
🤔 should we call this lib uwr :P

@quietshu sorry for the joke, I just couldn't resist...
I made a reproduction sandbox example, you can check it here https://codesandbox.io/s/swr-issue-192-qq880
I hope this will get fixed soon.
Thx!

@Svish
Copy link
Contributor Author

Svish commented Dec 20, 2019

It should probably be an option though, because in other cases it's probably a good thing that it does go through undefined. 🤔

I suppose making it an option would also mean it could be added without breaking any existing usage. 👍

@isBatak
Copy link

isBatak commented Dec 20, 2019

I'm not sure... data should always be stale.
If you need it to be undefined you can do this:
const weirdData = isValidating ? undefined : data;

@shuding shuding added the discussion Discussion around current or proposed behavior label Dec 20, 2019
@shuding
Copy link
Member

shuding commented Dec 20, 2019

Currently the data goes back to undefined whenever the key changes.

This is intended. The reason of this behavior is that although SWR returns stale data, it can't return wrong data. The data value should always be the result of that exact key.

But I agree here from the real world use cases, it can be an option to disable the strict key-data consistency.

@isBatak
Copy link

isBatak commented Dec 20, 2019

What do you mean by wrong data?

@Svish
Copy link
Contributor Author

Svish commented Dec 20, 2019

@isBatak If you're starting out a loaded const { data } = useSWR('foo', fetcher), and then switch the key to 'bar', then data, if it stuck around, would be the wrong data because it belongs to 'foo', not 'bar'.

So that should be the default, but yeah, would be great with an option to switch how that works.

@sergiodxa
Copy link
Contributor

If you ask SWR to give you /api/users?id=1 and then you ask for /api/users?id=2 in the component (let's say the id was a state) returning the data of the id 1 it's the wrong data, because the user expects the data of the user 2 and that is not yet cached, if you go back to the id 1 it will return the previously cached data while it's revalidating and this is working since I used it a lot of times.

I think this will be solved once useTransition is stable, then you will be able to wrap the state change of the id in a startTransition and let React keep rendering the component with id 1 until the data of 2 arrives, or a lot of time has passed.

@Svish
Copy link
Contributor Author

Svish commented Dec 20, 2019

@sergiodxa What is this useTransition you speak of? Sounds interesting. 🤔

@sergiodxa
Copy link
Contributor

It's a new React hook coming with Concurrent Mode: https://reactjs.org/docs/concurrent-mode-reference.html#usetransition

A component using it together with SWR would look like this:

const SUSPENSE_CONFIG = { timeoutMs: 1000 };

function User() {
  const [id, setID] = React.useState(null);
  const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
  const { data } = useSWR(id ? `/api/users?id=${id}` : null, fetcher, { suspense: true });

  function handleChange(event) {
    const newID = event.target.value;
    startTransition(() => {
      setID(newID);
      mutate(`/api/users?id=${newID}`, fetcher(`/api/users?id=${newID}`));
    });
  }

  return (
    <>
      <input type="text" onChange={handleChange} value={id} />
      {isPending && <Spinner />}
      {data && 
        <h1>{data.username}</h1>
      }
    </>
  );
}

Something like that, without trying to run it so it may had typos, the idea here is to to tell React keep rendering what it's already on screen until the new UI triggered by setID and mutate it's ready or the value of timeoutMs has passed (a second in the example), meanwhile you get the isPending boolean to show to the user you are updating it but without clearing the old data.

@isBatak
Copy link

isBatak commented Dec 20, 2019

if we pass array as key, for example ['foo', fetcherDep1, fetcherDep2], could we treat first element as a key and the rest as deps for revalidation?

@Svish
Copy link
Contributor Author

Svish commented Dec 20, 2019

I think it's better to just add a stickyResult: boolean option, rather than making that key/deps thing even more confusing 😛

@shuding
Copy link
Member

shuding commented Dec 26, 2019

For now you can use a custom hook for that:

function useStickyResult (value) {
  const val = useRef()
  if (value !== undefined) val.current = value
  return val.current
}

Together with SWR:

const { data } = useSWR('/user?' + id)
const stickyData = useStickyResult(data)

Or combine them into a new hook:

function useStickySWR (...args) {
  const swr = useSWR(...args)
  const stickyData = useStickyResult(swr.data)
  swr.data = stickyData
  return swr
}

Adding an option stickyResult: boolean is a bit tricky because we need to consider errors too.

@shuding shuding added the feature request New feature or request label Jan 2, 2020
@isBatak
Copy link

isBatak commented Jan 7, 2020

@quietshu is this custom hook still relevant?
I'm asking because of this merge #186
swr.data is getter now, and isValidating is always false for some reason...

@stevewillard
Copy link

Ran into the same issue, and I don't think the above useStickySWR works, since as you mentioned @isBatak, you can't modify it.

Has any else worked around this? I want to differentiate initial loading vs reloading (with maybe slightly different params).

@isBatak
Copy link

isBatak commented Jan 31, 2020

@stevewillard I changed useStickySWR a bit

import { useRef } from 'react';
import useSWR from 'swr';

export function useStickySWR(...args) {
  const val = useRef();

  const { data, isValidating, error, ...rest } = useSWR(...args);

  if (data !== undefined) {
    val.current = data;
  }

  return {
    ...rest,
    isValidating,
    error,
    data: val.current,
  };
}

But now it is deoptimized and will re-render component whether you use some pros or not.
But in my case, it is good enough solution.

@raj-altrio
Copy link

How would this work in Suspense mode?
If I want the sticky hook to work with suspense would I just catch the promise from useSWR here instead of checking if data is undefined?

@shuding shuding added this to the 1.0 milestone Sep 20, 2020
@bobbyhadz
Copy link

Is there a good way to do this now?

Can we keep wrong data rendered until we fetch right data, useTransition style?

I.e. in pagination, it seems a very bad user experience to show the loading skeleton (the suspense fallback) between page transitions, I'd much rather display a loading spinner to indicate a transition is taking place.

I'm new to swr, so apologize if I'm missing something.

@RobbyUitbeijerse
Copy link

RobbyUitbeijerse commented Apr 17, 2021

@bobbyhadz I would consider the solution provided by @isBatak by resolving this through a ref 'good enough' in a sense that it works, but it would indeed be a great feature to have withint SWR itself.

My current situation is as follows:

  1. On the server (in the build pipeline) I'm getting both items and filters - both returned by a single endpoint
  2. I'm passing the data through initialData to hydrate the SWR cache and render the UI during build
  3. When a user filters using one of the filter options, we build a unique key based on the combined filter options and revalidate the data
  4. In the new response, only filters that will actually lead to actual results will be present (filters that will lead to 0 results will be removed)

This all works ok, but it results in the following quirk that could be considered weird/buggy from a user perspective:

While filtering, during revalidating, the user sees vacancies and filter options that were initially provided through initialData. Even when these items and filters might not even be relevant to the user anymore. In case of a mediocre performing API / a slow connection this just feels off/unacceptable.

Here's a clip of the situation:
https://user-images.githubusercontent.com/10176709/115119553-3ad10b80-9fa9-11eb-92d2-55a7ba0b1802.mp4

Solution for my situation, inspired by comments above

const useData = () => {
 const mutableRef = useRef();

 const { data, ...rest } = useSWRInfinite(key, fetcher, {
   initialData: mutableRef.current ? mutableRef.current : swrConfig.initialData,
 })
   
 if (data !== undefined && mutableRef) {
   mutableRef.current = data;
 }

 return { data, ...rest }
}

@huozhi huozhi modified the milestones: 1.0, Backlog Jul 21, 2021
@huozhi huozhi removed the discussion Discussion around current or proposed behavior label Jul 21, 2021
@shuding
Copy link
Member

shuding commented Aug 28, 2021

We've made a handle middleware for this use case: Keep Previous Result. You can easily customize it based on your own use case.

Please also check the SWR 1.0 blog post! 🎉

@shuding
Copy link
Member

shuding commented Jan 28, 2022

Reopening as I think this can be a good built-in feature.

@rauchg
Copy link
Member

rauchg commented Feb 17, 2022

Also worth noticing that this is impacting us when you press "back" and it loses your search state.

CleanShot 2022-02-16 at 16 01 10

@Macfly
Copy link

Macfly commented Mar 8, 2022

handleChange

is the middle still the best way to handle this kind of situation?

@dqunbp
Copy link

dqunbp commented Apr 6, 2022

@shuding I have the same problem, in my case I use fallbackData, and the data returns the fallbackData content for each invalidation, even with laggy middleware

@steven-tey
Copy link

steven-tey commented Apr 11, 2022

Facing the same issue as @dqunbp! When I'm using the fallbackData option, it still returns the fallbackData content for some reason (even with laggy middleware). When I remove fallbackData, it works.

I believe the issue lies in the following line:

    const dataOrLaggyData =
      swr.data === undefined ? laggyDataRef.current : swr.data;

In the case where fallbackData is present, swr.data === undefined should be something like swr.data === fallbackData

@dqunbp
Copy link

dqunbp commented Apr 11, 2022

@steven-tey This is my workaround to pass fallbackData initial only

I also wrap it to my custom useQuery hook, with additional features:

  • transform prop to transform response
  • if fallbackData option passed, type of data does not contain undefined
Hook code. Click to expand!
import { AxiosError as A, AxiosResponseTransformer as ART } from 'axios'
import { useRef } from 'react'
import useSWR, { SWRConfiguration } from 'swr'
import { SWRKey } from '../createSWRConfig'
import { SWRResult, SWRResultWithFallback } from '../types'
import createTransformMiddleware from './middlewares/transform'
import laggy from './middlewares/laggy'

type Config<D, E> = SWRConfiguration<D, E> & {
  transform?: ART
}

type LaggyProps = { isLagging: boolean; resetLaggy(): void }

// Config without fallback & Response without fallback
// OF - omit fallback
// OFR - omit fallback response
type OF<D, E> = Omit<Config<D, E>, 'fallbackData'>
type OFR<D, E> = SWRResult<D, E> & LaggyProps

// Config with fallback & Response with fallback
// F - fallback
// FR - fallback response
type F<D, E> = OF<D, E> & { fallbackData: D }
type FR<D, E> = SWRResultWithFallback<D, E> & LaggyProps

// useQuery result depends on fallback type
// QC - query config
// QR - query response
type QC<D, E> = Config<D, E>
type QR<D, E> = D extends undefined ? OFR<D, E> : FR<D, E>

function useQuery<D, E = A>(key: SWRKey | null, options?: OF<D, E>): OFR<D, E>
function useQuery<D, E = A>(key: SWRKey | null, options?: F<D, E>): FR<D, E>

function useQuery<D, E = A>(key: SWRKey | null, options?: QC<D, E>): QR<D, E> {
  const { url = '', params = {} } = key || {}
  const { fallbackData, transform, use = [], ...restOptions } = options || {}

  const fallback = useRef(fallbackData)

  const transformMiddleware = createTransformMiddleware(transform)

  const response = useSWR<D, E, SWRKey | null>(key ? { url, params } : null, {
    ...restOptions,
    use: [transformMiddleware, laggy, ...use],
  })

  const { data, error, mutate, isValidating, isLagging, resetLaggy } =
    response as QR<D, E>

  return {
    data:
      typeof data === 'undefined' && typeof fallback.current !== 'undefined'
        ? fallback.current
        : data,
    error,
    mutate,
    isValidating,
    isError: Boolean(error),
    isLoading: !error && !data,
    isLagging,
    resetLaggy,
  } as QR<D, E>
}

export default useQuery
Types. Click to expand!
type SWRResponseOmitData<Error> = Omit<SWRResponse<unknown, Error>, 'data'>

export type SWRResult<Data, Error> =
  | ({
      data: undefined
      isLoading: true
      isError: false
    } & SWRResponseOmitData<Error>)
  | ({
      data: Data
      isLoading: false
      isError: boolean
    } & SWRResponseOmitData<Error>)

export type SWRResultWithFallback<Data, Error = unknown> = {
  data: Data
  isLoading: boolean
  isError: boolean
} & SWRResponseOmitData<Error>
Default fetcher. Click to expand!
import { AxiosRequestConfig, AxiosResponseTransformer } from 'axios'

import { APIData } from 'types/commonTypes'
import createAPIClient from './apiClient/create'

export type SWRKey = {
  url: string
  params?: AxiosRequestConfig['params']
}

const createSWRConfig = (apiData: APIData) => ({
  fetcher: async (
    { url, params }: SWRKey,
    transformResponse?: AxiosResponseTransformer
  ) => {
    const client = createAPIClient(apiData)

    const { data } = await client.get(url, {
      transformResponse,
      data: {}, // required to add Content-Type to request
      params,
    })

    return data
  },
})

export default createSWRConfig
Transform middleware. Click to expand!
import { Middleware } from 'swr'
import { AxiosResponseTransformer } from 'axios'
import { SWRKey } from 'requestSystem/createSWRConfig'

const createTransformMiddleware =
  (transform?: AxiosResponseTransformer): Middleware =>
  (next) =>
  (k, fetcher, config) => {
    const extendedFetcher = fetcher
      ? (kk: SWRKey) => fetcher(kk, transform)
      : fetcher
    return next(k, extendedFetcher, config)
  }

export default createTransformMiddleware

P.S. laggy middleware used from the docs as is

@dqunbp
Copy link

dqunbp commented Apr 11, 2022

Facing the same issue as @dqunbp! When I'm using the fallbackData option, it still returns the fallbackData content for some reason (even with laggy middleware). When I remove fallbackData, it works.

I believe the issue lies in the following line:

    const dataOrLaggyData =
      swr.data === undefined ? laggyDataRef.current : swr.data;

In the case where fallbackData is present, swr.data === undefined should be something like swr.data === fallbackData

I don't think there are bugs within laggy middleware
I think it's not an option to handle this the way you want with laggy middleware because that middleware also receives fallbackData as data.
There is no way to understand which of incoming to middleware data is fallback or not.

@shuding shuding modified the milestones: backlog, 2.0 Apr 12, 2022
@shuding
Copy link
Member

shuding commented Apr 12, 2022

Aiming to add a built-in solution for this in the upcoming 2.0 version.

@shuding
Copy link
Member

shuding commented Apr 15, 2022

My first step is to add #1925, which completes the necessary states to implement laggy and fallback. And then, we will look for a way to enable the laggy middleware via just one option { laggy: true }. The only thing bothers me is we can't easily tree-shake it away when not used, maybe not a big deal :)

@steven-tey
Copy link

steven-tey commented Apr 16, 2022

Can confirm that adding keepPreviousData: true as an option in SWRConfiguration works perfectly for me 🤩

const { data, isValidating } = useSWR(
    `/api/endpoint?page=${page}`,
    fetcher,
    {
      keepPreviousData: true,
      fallbackData: props.data,
    }
);

Note: this feature is in pre-release so you'll have to do npm i swr@2.0.0-beta.1 to be able to access it.

@dqunbp can you try downloading the latest version, try it out and let me know if it works? :)

@ItsTarik
Copy link

Very useful when paginating tables !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.