swr: Data is not updated with `initialData`

I’m on a Next.js app and here is my (simplified) code:

const IndexPage = ({ data: initialData }) => {
  const [filters, setFilters] = useState(defaultFilters)

  const onChange = () => {
    ...
    setFilters(newFilters)
  }

  const query = getQuery(filters)
  const { data } = useSWR(`/api/resorts?${query}`, fetcher, { initialData })

  return (...)
}

Index.getInitialProps = async ctx => {
  const query = getQuery(defaultFilters)
  const data: Resort[] = await fetcher(`${getHost(ctx)}/api/resorts?${query}`)
  return { data }
}

I have an initial set of filter (defaultFilters) on which I query and pass to useSWR with initialData on first render. When the user changes one filter, the key should change as it’s a new query, but useSWR still returns the old data, from the first call.

When I remove initialData from useSWR it works but that’s not what I want, I want to have SSR.

Am I doing something wrong?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 28
  • Comments: 57 (10 by maintainers)

Most upvoted comments

I don’t feel this is the way hooks work in this case. Almost the same argument would be to say useState default value should be set to undefined by developer once you do first setState or you would get the unchanged state all the time.

Even more initialData is explicitly used as example for SSR. In this case the data is provided once and should be considered irrelevant once the key changes. Anytime I change the key I want to fetch new data and I don’t care about initialData anymore.

If this is not a bug then maybe at least a suggestion for improvement in docs? Additionally some other prop could be added to api alongside initialData

Whether this is a bug or intended, I think it should be documented properly (and same goes for the currently nowhere mentioned useSWRPages).

I think the real problem here is “fallback” vs “cache”. initialData was designed to be per hook and it was considered as “fallback”. Let’s say 2 components:

function A() {
  useSWR('data', fetcher, { initialData: 1 })
}
function B() {
  useSWR('data', fetcher, { initialData: 2 })
}

It will be very confusing if initialData (the fallback) is bound to the key, if the keys are the same / are changing.

Ideally, a better API design should be context (note this API doesn’t exist):

<SWRConfig initialData={initialData}> // a key-value object
  <App/>
</SWRConfig>

And all the SWR hooks inside the tree should use it as the data source.

Thanks for the suggestion! I applied the “undefined” fix.

I think the bug here, if it’s a bug and not intentional, is that SWR should detect the key changed and ignore the initialData, only use the cache.

Indeed it seems to be a bug because it’s not intuitive and not documented either…

For googlers who are puzzled when they have set initialData hoping for SWR to push fresh data when necessary (while also possibly fetching initialData in getServerSideProps), I found that setting revalidateOnMount: true (as suggested by @Romstar ) solved the issue of updating a page with e.g. a new item. It’s worth a try if that’s your issue, before going on to some of the more complex solutions suggested by other people here (at least it solved my update issue on page navigation).

Here’s a fully Typescript version of what @isaac-scarrott wrote for a workaround:

import { useEffect, useRef } from 'react';
import _useSwr from 'swr';
import { ConfigInterface, keyInterface, fetcherFn, responseInterface } from 'swr/dist/types';

/**
 * Patched version of SWR to work around bug where a key change is not detected when using initial data
 * @see https://github.com/vercel/swr/issues/284
 */
const useSwr = <Data, Error>(
  key: keyInterface,
  fn?: fetcherFn<Data>,
  config?: ConfigInterface<Data, Error>
): responseInterface<Data, Error> => {
  const hasMounted = useRef(false);

  useEffect(() => {
    hasMounted.current = true;
  }, []);

  return _useSwr<Data, Error>(key, fn, {
    ...config,
    initialData: hasMounted.current ? undefined : config?.initialData,
  });
};

export default useSwr;

I also agree with @nfantone that this needs addressing as it seems like it’s going to continue causing significant issues going forward. I think we’re needing agreement from the maintainers on what the expected behaviour so we can start working on a fix.

I was against this at first, but the more I think about it the more I realise it goes against what I fell in love with about useSWR: it’s incredibly simple but insanely flexible.

I solve my use-case like this:

  const { data } = useSWR(key, fetcher, {
    initialData: key === initialKey ? initialData : undefined,
  })

initialKey is my initial algolia query formatted to a string that is passed to a simple fetch() which gets initialData in getServerSideProps. key is the updated version of the algolia query (whenever a user searches or changes a filter) that signals to useSWR that its time to take over. Whenever key doesn’t equal initialKey we know initialData is no longer relevant.

Sure, the initialData option is not intuitive for the SSR use-case, but adding a new option for every unintuitive case (that can be solved in user-land) that we discover will slowly morph this package into the the very thing we are escaping (atleast, what I am escaping anyway 😄)

I replicated it here https://codesandbox.io/s/eager-lovelace-hgj9h

A fix right now is to grab mutate from useSWR results and call it immediately. In the CodeSandbox there is a fix running useEffect to call the mutate function and revalidate automatically.

Another fix, also in the CodeSandbox, is to change initialData to undefined if the dynamic part (in your case the query) is not the same as the initial one, to do this in your case you will need to pass the “initialQuery” (the one from getInitialProps) to the component as a prop and use it to detect if you should use or no initialData.

I think this makes sense, SWR is using the initialData if there is no a cached data already as the value of your current key, when you change the key is using the same initialData for the new key.

I think the bug here, if it’s a bug and not intentional, is that SWR should detect the key changed and ignore the initialData, only use the cache.

I’ve made a custom hook/wrapper around useSwr that fixes this issue with Next.js if anyone is interested:

import { useEffect, useRef } from 'react'
import _useSwr from 'swr'

const useSwr = (key, fn, config) => {
  const hasMounted = useRef(false)

  useEffect(() => {
    hasMounted.current = true
  }, [])

  return _useSwr(key, fn, {
    ...config,
    initialData: hasMounted.current ? undefined : config.initialData,
  })
}

export default useSwr

Whether or not it’s a bug, it’s clearly unexpected for quite a few people, and prevents one of the more common use-cases of server side rendering of dynamic routes without an unintuitive workaround. @shuding’s proposal looks to be a good one (and maybe we could rename the argument to SWRConfig “cache” for clarity?), so hopefully something like that lands soon.

(revalidating the initial data by default would lead to a second unnecessary request on first load for server side rendered pages - I believe that’s what the behaviour used to be and was changed, for this reason.)

I agree and I don’t see any good use case for current behaviour. If for each key there is always initial data updated on the server side then why do I need using this hook at all?

I also ran into this problem and used the workaround from @danielrbradley which worked great. It uses some deprecated types though which I had to update. My version ended up looking like this:

import { useEffect, useRef } from 'react';
import _useSwr, { Key } from 'swr';
import { Fetcher, SWRConfiguration, SWRResponse } from 'swr/dist/types';

/**
 * @see https://github.com/vercel/swr/issues/284
 * @see https://github.com/vercel/swr/issues/284#issuecomment-706094532
 */
export const useSWR = <Data = never, Error = never>(
  key: Key,
  fn?: Fetcher<Data>,
  config?: SWRConfiguration<Data, Error>,
): SWRResponse<Data, Error> => {
  const hasMounted = useRef(false);

  useEffect(() => { hasMounted.current = true; }, []);

  return _useSwr<Data, Error>(key, fn, {
    ...config,
    initialData: hasMounted.current ? undefined : config?.initialData,
  });
};

We released SWR 1.0 with better preloaded cache solution. Checkout cache doc for details.

For fallback purpose, initialData is renamed to fallbackData if you don’t need any preloaded state in cache.

I can’t believe this bit me again today 😥. A customer raised an issue explaining that they “weren’t seeing the new email entry in their table after creating it”. And, lo and behold, it was from an older project where the workaround proposed here for initialData was not in place.

At this point, I believe this issue has been open for long enough and really needs to be addressed. I’m open to putting together myself a PR to integrate the didMount solution into swr, if there’s an interest for it.

Let’s hear from collaborators.

OK so if I understand correctly currently the logic is that if I provide initialData for a hook then both for initial key and future keys per this hook it’s considered a fallback if current key’s data is not in cache.

In my understanding the key is often identifying data itself like for example user/profile/1234 or products/jeans. If I change the key to other user profile or product category I really do not expect the initialData try to provide defaults for me anymore.

I think this does make sense and is not confusing right? It’s irrelevant what keys other hooks are using.

I’m on a Next.js app and here is my (simplified) code:

const IndexPage = ({ data: initialData }) => {
  const [filters, setFilters] = useState(defaultFilters)

  const onChange = () => {
    ...
    setFilters(newFilters)
  }

  const query = getQuery(filters)
  const { data } = useSWR(`/api/resorts?${query}`, fetcher, { initialData })

  return (...)
}

Index.getInitialProps = async ctx => {
  const query = getQuery(defaultFilters)
  const data: Resort[] = await fetcher(`${getHost(ctx)}/api/resorts?${query}`)
  return { data }
}

I have an initial set of filter (defaultFilters) on which I query and pass to useSWR with initialData on first render. When the user changes one filter, the key should change as it’s a new query, but useSWR still returns the old data, from the first call.

When I remove initialData from useSWR it works but that’s not what I want, I want to have SSR.

Am I doing something wrong?

I ended up using this config option: revalidateOnMount: true.

initialData: initial data to be returned (note: This is per-hook)

revalidateOnMount: enable or disable automatic revalidation when component is mounted (by default revalidation occurs on mount when initialData is not set, use this flag to force behavior)

Example:

const { data: listOfUsers, mutate: setListOfUsers } = useSWR(API_ROUTES.ADMIN.USERS, { fetcher, initialData: [], revalidateOnMount: true });

and now my data is initialized as an empty array and the data is pulled after the first render which then causes the state to be updated and my array is populated with real data which then allows my app to work properly!

Now with swr@beta, If you’re using next.js, you can use custom cache API to initialize the very first state

import { createCache } from 'swr'

let cacheProvider

export default function IndexPage({ initialState }) {
  if (!cacheProvider) {
    cacheProvider = createCache(new Map(initialState))
  }
  return (
    <SWRConfig value={{ cache: cacheProvider.cache }}>
      <Index />
    </SWRConfig>
  );
}
export async function getServerSideProps() {
  return {
    props: {
      initialState: [ [key1, value1], [key2, value2], [key3, value3] ]  /* initial states */
    }
  };
}

Fell into the same rabbit hole as everybody else here.

I’m implementing a basic search results page where the first set of results is fetched server side (for SEO purposes). The way useSWR currently works with initialData means that while the very first list of results is returned just fine, new searches keep returning the same items. For some reason, I found that it won’t even trigger network requests even if the key changes, in some cases.

@isaac-scarrott proposed solution actually worked for me 👍.

import { useEffect, useRef } from 'react';
import _useSwr from 'swr';

/**
 * Fetches data using a "stale-while-revalidate" approach.
 *
 * @see https://github.com/vercel/swr
 * @param {import('swr').keyInterface} key A unique key string for the request (or a function / array / `null`).
 * @param {import('swr').ConfigInterface} [options={}] An object of options for this SWR hook.
 * @returns {import('swr').responseInterface} SWR response map.
 */
function useSwr(key, config = {}) {
  // We pass `initialData` to `useSwr` on the first render exclusively
  // to prevent SWR from returning the same cached value if `key` changes
  const didMount = useRef(false);

  useEffect(() => {
    didMount.current = true;
  }, []);

  return _useSwr(key, {
    ...config,
    initialData: didMount.current ? undefined : config.initialData
  });
}

export default useSwr;

The only downside I see is that, unless using TS, you lose typings and intellisense on VSCode.

@lkbr

I solve my use-case like this:

  const { data } = useSWR(key, fetcher, {
    initialData: key === initialKey ? initialData : undefined,
  })

Right - but then you’d always be returning initialData for the same original key, no matter when you do it, which might have become stale at some point.

@Romstar As pointed out by @majelbstoat some comments ago, if you go down that route you’ll be triggering a second request for basically the same data - preventing that is one of the upsides of using SSR in the first place.

(revalidating the initial data by default would lead to a second unnecessary request on first load for server side rendered pages - I believe that’s what the behaviour used to be and was changed, for this reason.)

@shuding

@quietshu, so under that approach, and using with NextJS, we’d do getServersideProps (well, getInitialProps until gSP is supported on _app.tsx and _document.tsx), return initialData and then pass that to a SWRConfig defined in _app.tsx or _document.tsx. Is that right?

Yup.

IMHO, I don’t feel like this would be a smart way to go about this moving forward. Not only it would break logic and data locality significantly, but as the current Next.js API stands, it would also completely void the ability to statically optimize pages in your app that could benefit from it, right?

@pke I used useRef as this will not trigger a rerender where as useState will trigger a rerender. In this case we don’t want to UI to update when we change the value of hasMounted to true, so by using useRef instead of useState when we update the ref value this will not trigger a rerender and created a very small performance increase.

There is the another workaround using cache. It allows to delete a cache entry synchronously before initialization of useSWR hook, so it removes the useless rerender from other solutions:

import useSWR, { cache } from 'swr';

export function usePagination(initialData) {
  const mounted = React.useRef(false);
  const key = '/list';

  if (cache.has(key) && !mounted.current) {
    cache.delete(key);
  }

  mounted.current = true;

  return useSWR(key, { initialData });
}

However, it doesn’t delete other SWR cache entries for isValidation and error states. If you need that, there is cache.serializeKey method that returns additional keys.

@jacobedawson thank you for the tip! revalidateOnMount: true does revalidate.

My issue now is that I’m fetching the same data twice in a row on the client for every client-side page route, once in getInitialProps (which is thrown out) and once again right after that when swr revalidates on mount. I’m considering detecting if the page is being rendered SSR and only fetching if the render is on the server.