# Apollo Client shows stale data after leaving current screen

**URL:** <https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633>\
**Category:** Client SDKs\
**Tags:** client, web\
**Created:** [February 3, 2023, 9:16pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633 "2023-02-03T21:16:33Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 3, 2023, 9:16pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/1 "2023-02-03T21:16:33Z")

</div>

Hi there!

I’m facing an issue when implementing Apollo Client on a React Native app.

I’m fetching a list of Listing entities from our server, using offset based pagination (with a limit of 5) to show the user a vertical list that appends new pages to the bottom.

I’ve set the necessary merge and read functions in the query’s type policy. This works fine – results are properly fetched and displayed, and whenever I fetchMore, they are properly merged, 5 by 5, and I can reach the end of the list (in this test case, a total of 12 listings) with no issues.

However, whenever I leave the screen (e.g. by clicking on a listing) and come back, only the first 5 results are displayed. I can see with a console.log() in my React component that immediately after I leave the screen, the number of Listings goes down from 12 to 5. The logs I’ve put in the read and merge functions are NOT accesed when this happens.

When I try to fetchMore, the logs ARE accessed, and I can see that the full 12 listings are returned from both functions; however, the number of Listings in `data` returned by the useLazyQuery hook remains fixed at only 5.

I’ve read the docs up and down and have not found a solution. I’m at my wit’s end, I do not know how to proceed and am seriously considering replacing Apollo Client with a different solution such as Tanstack Query, but I would much rather prefer not having to do so.

Any help will be appreciated. Thanks!

---

<div class="post-metadata">

**Author:** ![JeffAuriemma](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/jeffauriemma/32/2845_2.png) [@JeffAuriemma](https://community.apollographql.com/u/JeffAuriemma)\
**Post date:** [February 10, 2023, 4:17am UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/2 "2023-02-10T04:17:23Z")

</div>

Hi Nico 👋 welcome to the forum! I’m guessing you’re passing the offset/limit values in `variables` somewhere, yes? If so, when you navigate away from and back into that component, are the `variables` values being reset? E.g.

1. Last state: `offset: 10, limit: 5`
2. Navigate away
3. Return to component with paginated data
4. New state: `offset: 0, limit: 5` (?)

I apologize if these assumptions are not correct - might you be able to share some code snippets so that I can see what you see?

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 10, 2023, 3:01pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/3 "2023-02-10T15:01:06Z")

</div>

Hi Jeff!

I actually found more info while debugging:

When you tap a list item and go to the individual listing screen, a different query is executed. I found out that, if I remove the field id inside that query, the bug stops happening. I don’t understand why though.

```auto
query GetObjectDetails($objectId: String!) {
  object(id: $objectId) {
    product {
      name
    }
    photos {
      id
      deletedDescription
      url
      mainPhoto
      isInternal
    }
    id --> Removing this field stops the bug from happening
    name
    description
    objectStatus {
      id
      description
    }
    guid
  }
}

```

Both queries handle the same type of entity, the typename in the cache is \_\_ObjectEntity. However, the ones in the list are under the \_\_PaginatedObjectEntities umbrella, I still don’t understand why a single object would overwrite the cache for the list of objects.

I’ve just now fixed the bug by adding this to my type policies:

```auto
  ObjectEntity: {
    keyFields: false,
  },

```

If I instead use or [‘id’] or [‘guid’] or [‘id’, ‘guid’], it still doesn’t work, and I get the same bug as before, even though they are unique values and should be unique identifiers in the cache.

I still don’t understand WHY this works. Could you please elaborate?

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 10, 2023, 3:02pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/4 "2023-02-10T15:02:25Z")

</div>

I recorded a video before finding out that the second query was responsible for the issue, but it’s still good for you to understand the way in which the cache is misbehaving

[![](https://us1.discourse-cdn.com/flex019/uploads/apollographql/original/2X/f/f52d0c6f516e9f69f42110d9296cf7f2d7a983de.jpeg "Apollo help request") ](https://www.youtube.com/watch?v=HqnOR1NXLeU)

---

<div class="post-metadata">

**Author:** ![JeffAuriemma](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/jeffauriemma/32/2845_2.png) [@JeffAuriemma](https://community.apollographql.com/u/JeffAuriemma)\
**Post date:** [February 10, 2023, 9:29pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/5 "2023-02-10T21:29:15Z")

</div>

Thanks for the info @nico - I watched the video and unfortunately I think I’m still missing some info that might be helpful. Your `ObjectInventory` component code may be relevant, as might the `merge` function you reference in the video.

Setting `keyFields: false` excludes your `ObjectEntity` objects from normalization, meaning that they won’t be deduplicated and stored as refs in your cache. So your `object` field will just contain a nested object with values that may be duplicates of values in your `objects` field.

My hypothesis is that, without `keyFields: false`, `GetObjectDetails` is causing a merge to run on entities in your `objects` field (this is expected behavior), which will then trigger a rerender of components that are watching that `objects` query. I wonder whether the `ObjectInventory` is preserving the `offset` value on rerender? Again, it’s tough to say without being able to inspect the code more closely. I hope that helps! Happy to keep taking a look at things, I’m sure we’ll be able to find a solution.

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 13, 2023, 4:07pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/6 "2023-02-13T16:07:54Z")

</div>

That’s awesome Jeff, here’s the code for both screens and for the merge funcion. Hope you can find the issue!

ObjectInventory:

```auto
const PAGINATION_LIMIT = 5

export const ObjectInventoryScreen: React.FC<RootStackScreenProps<'ObjectInventory'>> = ({ navigation }) => {
  const { strings } = useLocalization()
  const [selectedObjects, setSelectedObjects] = React.useState<ObjectEntity[]>([])
  const { onboardingDismissed: salesFlowOnboardingDismissed } = useSalesFlowOnboarding()
  const { user } = useAuth()

  const [filterSheetVisible, setFilterSheetVisible] = React.useState(false)
  const [selectedStatusFilterIds, setSelectedStatusFilterIds] = React.useState<AllowedObjectStatuses[]>([])

  const toggleStatusFilter = (filterId: AllowedObjectStatuses) => {
    if (selectedStatusFilterIds.includes(filterId)) {
      return setSelectedStatusFilterIds(selectedStatusFilterIds.filter((fid) => fid !== filterId))
    } else {
      return setSelectedStatusFilterIds([...selectedStatusFilterIds, filterId])
    }
  }

  const toggleObject = (object: ObjectEntity) => {
    if (selectedObjects.find((selectedObject) => selectedObject.id === object.id)) {
      return setSelectedObjects((selected) => selected.filter((selectedObject) => selectedObject.id !== object.id))
    } else {
      return setSelectedObjects((selected) => [...selected, object])
    }
  }

  const [
    getInventoryObjects,
    { data: objectsData, loading: objectsLoading, error: objectsError, fetchMore: fetchMoreObjects },
  ] = useGetInventoryObjectsLazyQuery({
    notifyOnNetworkStatusChange: true,
  })

  const actualObjects = objectsData?.objects.items

  useEffect(() => {
    if (user?.realId && !objectsData) {
      getInventoryObjects({
        variables: {
          userId: user.realId,
          filters: {
            status: selectedStatusFilterIds,
          },
          options: {
            limit: PAGINATION_LIMIT,
          },
        },
      })
    }
  }, [user?.realId, selectedStatusFilterIds])

  const sellableSelectedObjects = selectedObjects.filter((obj) => obj.product.eligibleForSale)

  const objectActions = [
    // {
    // label: 'Devolución',
    // onPress: () => console.log('Lorem'),
    // icon: TruckSvg,
    // },
    {
      label: 'Solicitar venta',
      badgeText: sellableSelectedObjects.length,
      disabled: sellableSelectedObjects.length === 0,
      onPress: () => {
        const nextScreen: NavigatorScreenParams<SaleFlowStackParamList> = {
          screen: 'ObjectSummary',
          params: { objectsSetExternally: clone(sellableSelectedObjects) },
        }

        if (!salesFlowOnboardingDismissed) {
          navigation.navigate('SaleFlow', { screen: 'SaleFlowOnboarding', params: { followingScreen: nextScreen } })
        } else navigation.navigate('SaleFlow', nextScreen)
        setSelectedObjects([])
      },
      icon: ShoppingBag,
    },
    // {
    // label: 'Donar',
    // onPress: () => console.log('Lorem'),
    // icon: null,
    // },
    // {
    // label: 'Poner en alquiler',
    // onPress: () => console.log('Lorem'),
    // icon: null,
    // },
  ]

  const showActionBar = !!selectedObjects.length

  return (
    <ViewWithSafeAreaInsets style={[styles.container]}>
      <View style={styles.filterBar}>
        <Button
          icon={
            filtersOnlyOnSheet > 0
              ? () => (
                  <View style={styles.filterBadge}>
                    <Text type='body2' weight='medium' paletteColor='contrast'>
                      {filtersOnlyOnSheet}
                    </Text>
                  </View>
                )
              : 'filter-variant'
          }
          contentStyle={styles.filterButtonContent}
          onPress={() => setFilterSheetVisible(true)}
          style={styles.filterButton}>
          {strings.generic.filter}
        </Button>

        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          {filtersForMainView.map((filter) => (
            <Chip
              mode='outlined'
              compact
              key={filter.value}
              selected={!!selectedStatusFilterIds.find((sf) => sf === filter.value)}
              onPress={() => toggleStatusFilter(filter.value as AllowedObjectStatuses)}>
              {filter.label}
            </Chip>
          ))}
        </ScrollView>
      </View>
      <View style={styles.topBar}>
        <Text type='body2' weight='medium'>
          {objectsLoading
            ? `${strings.generic.loading}...`
            : actualObjects?.length
            ? strings.ObjectInventoryScreen.objectTotal(actualObjects?.length ?? 0)
            : ''}
        </Text>
      </View>

      <View style={{ flex: 1 }}>
        {objectsLoading && !actualObjects?.length ? (
          <>
            <LoadingLargeObjectCard />
            <LoadingLargeObjectCard />
            <LoadingLargeObjectCard />
            <LoadingLargeObjectCard />
          </>
        ) : null}
        {actualObjects?.length ? (
          <FlatList
            data={actualObjects}
            keyExtractor={(o) => o.id.toString()}
            ListFooterComponent={() => (objectsLoading ? <LoadingLargeObjectCard /> : null)}
            onEndReached={() => {
              const endReached = actualObjects.length >= objectsData!.objects.total
              if (objectsLoading || endReached) return

              fetchMoreObjects({
                variables: {
                  options: {
                    page: getNewPage(actualObjects.length, PAGINATION_LIMIT),
                    limit: PAGINATION_LIMIT,
                  },
                },
              })
            }}
            renderItem={({ item }) => (
              <LargeObjectCard
                object={item}
                disableAnimations
                onPress={() => navigation.navigate({ name: 'ObjectDetails', params: { objectGuid: item.guid } })}
                checked={!!selectedObjects.find((sf) => sf.id === item.id)}
                onCheck={() => toggleObject(item)}
                onLongPress={() => toggleObject(item)}
              />
            )}
          />
        ) : null}
        {!objectsLoading && !actualObjects?.length ? (
          <EmptyState title={strings.ObjectInventoryScreen.emptyState} />
        ) : null}
      </View>
      <ActionSheet actions={objectActions} visible={showActionBar} onClose={() => {}} />
      <FilterSheet
        filterCategories={FILTER_CATEGORIES}
        selectedFilterIds={selectedStatusFilterIds}
        visible={filterSheetVisible}
        onClose={() => setFilterSheetVisible(false)}
        onFilterClear={() => setSelectedStatusFilterIds([])}
        onFilterChange={(item) => toggleStatusFilter(item.value as AllowedObjectStatuses)}
      />
    </ViewWithSafeAreaInsets>
  )
}

const getNewPage = (currentLength: number, limit: number) => Math.ceil(currentLength / limit) + 1

```

ObjectDetails

```auto
export const ObjectDetailsScreen: React.FC<RootStackScreenProps<'ObjectDetails'>> = ({
  route: { params },
  navigation,
}) => {
  const width = Dimensions.get('window').width
  const { strings } = useLocalization()
  const [carouselIndex, setCarouselIndex] = React.useState(0)
  const [fabOpen, setFabOpen] = React.useState(false)
  const { navigate } = useNavigation()
  const isFocused = useIsFocused()
  const { onboardingDismissed: salesFlowOnboardingDismissed } = useSalesFlowOnboarding()
  const utilityStyles = useUtilityStyles()

  const { data, loading, error } = useGetObjectDetailsQuery({
    variables: { objectId: params.objectGuid },
  })

  if (error) return <EmptyState title={strings.ObjectDetailsScreen.error} />

  return (
    <ScrollView style={styles.container}>
      <View style={styles.carouselContainer}>
        {data?.object.photos && data.object.photos.length > 0 ? (
          <>
            <Carousel
              width={width}
              height={width}
              data={data?.object?.photos?.map((photo) => photo.url) ?? []}
              scrollAnimationDuration={200}
              loop={false}
              onSnapToItem={(index) => setCarouselIndex(index)}
              renderItem={({ item }) => (
                <View style={styles.carouselItem}>
                  <ImageWithLoader source={{ uri: item }} style={styles.carouselImage} />
                </View>
              )}
            />

            <View style={styles.carouselBottomInfo}>
              <View style={styles.carouselIndexBadge}>
                <Text style={styles.carouselIndexBadgeText}>{`${carouselIndex + 1}/${
                  data?.object.photos.length
                }`}</Text>
              </View>
              <ObjectStatusBadge statusId={data.object.objectStatus.description as AllowedObjectStatuses} />
            </View>
          </>
        ) : (
          <View style={[styles.noImage, { width, height: width }]}>
            <NoImageSvg />
            <Text type='caption'>{strings.ObjectDetailsScreen.photoEmptyState}</Text>
          </View>
        )}
      </View>

      <View style={styles.objectBasicInfoContainer}>
        <View style={styles.topInfo}>
          <View>
            {data?.object && !loading ? (
              <>
                <Text type='body'>ID: {data.object.id}</Text>
                <Text type='h1' weight='medium' marginTop={12}>
                  {data.object.product.name}
                </Text>
              </>
            ) : (
              <>
                <ContentLoader
                  speed={0.9}
                  width={250}
                  height={60}
                  viewBox='0 0 250 60'
                  backgroundColor='#f3f3f3'
                  foregroundColor='#ecebeb'>
                  <Rect x='0' y='0' rx='0' ry='0' width='80' height='16' key='id' />
                  <Rect x='0' y='28' rx='0' ry='0' width='100' height='28' key='title' />
                </ContentLoader>
              </>
            )}
          </View>
          <Pressable
            style={{ marginTop: 10 }}
            onPress={() => data?.object && navigate({ name: 'ObjectEdit', params: { object: data.object } })}>
            <EditSvg />
          </Pressable>
        </View>

        <View style={{ marginTop: 16 }}>
          {data && !loading ? (
            <>
              <Text type='body' marginBottom={16}>
                {data.object.name}
              </Text>
              <Text type='body2'>{data.object.description}</Text>
              {data.object.product.eligibleForSale && (
                <View style={[utilityStyles.flexRow, { marginTop: 24 }]}>
                  <ObjectPropBadge icon={<SaleBadgeSvg />} text={strings.ObjectDetailsScreen.propBadges.sellable} />
                </View>
              )}
            </>
          ) : (
            <>
              <ContentLoader speed={0.9} width={250} height={57} backgroundColor='#f3f3f3' foregroundColor='#ecebeb'>
                <Rect x='0' y='2' rx='0' ry='0' width='140' height='16' key='name' />
                <Rect x='0' y='38' rx='0' ry='0' width='120' height='14' key='description' />
              </ContentLoader>
            </>
          )}
        </View>
      </View>

      {data?.object?.product.eligibleForSale && !loading ? (
        <FABGroup
          open={fabOpen}
          visible={Boolean(isFocused)}
          handleOpenChange={setFabOpen}
          actions={[
            {
              icon: SaleBagSvg,
              label: 'Vender',
              onPress: () => {
                const nextScreen: NavigatorScreenParams<SaleFlowStackParamList> = {
                  screen: 'ObjectSummary',
                  params: { objectsSetExternally: [data!.object] },
                }

                if (!salesFlowOnboardingDismissed) {
                  navigate('SaleFlow', {
                    screen: 'SaleFlowOnboarding',
                    params: { followingScreen: nextScreen },
                  })
                } else navigate('SaleFlow', nextScreen)
              },
            },
          ]}
        />
      ) : null}
    </ScrollView>
  )
}

```

typePolicies

```auto
const typePolicies: StrictTypedTypePolicies = {
  Query: {
    fields: {
      objects: {
        keyArgs: ['userId', 'filters'],
        merge(existing, incoming, { args }) {
          const page: number = args?.options?.page ?? 1
          const limit: number | undefined = args?.options?.limit

          const existingItems: ObjectEntity[] = existing?.items ?? []
          const incomingItems: ObjectEntity[] = incoming?.items ?? []

          const mergedItems: ObjectEntity[] = existingItems.slice(0)

          if (page && limit) {
            const offset = (page - 1) * limit
            for (let i = 0; i < incomingItems.length; ++i) {
              mergedItems[offset + i] = incomingItems[i]
            }
          } else {
            mergedItems.push(...incomingItems)
          }

          const fullResult = {
            ...incoming,
            items: mergedItems,
          }

          return fullResult
        },
      },
    },
  },
  // ObjectEntity: {
  // // If I use [] as my keyFields, cache gets corrupted, even though the empty array is supposed to work the same as `false` as per the docs
  // // If I instead use or ['id'] or ['guid'] or ['id', 'guid'], it still doesn't work, and I get the same error as before, even though they are unique values
  // keyFields: false,
  // },
}

```

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 13, 2023, 4:08pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/7 "2023-02-13T16:08:33Z")

</div>

I think you may be right about the offset (page number in this case) not being preserved, but I don’t know what the correct practice would be in this case.

---

<div class="post-metadata">

**Author:** ![JeffAuriemma](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/jeffauriemma/32/2845_2.png) [@JeffAuriemma](https://community.apollographql.com/u/JeffAuriemma)\
**Post date:** [February 13, 2023, 4:12pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/8 "2023-02-13T16:12:39Z")

</div>

Thanks for sharing @nico - I will dig deeper into your code as soon as I can - it’s a busy day though so it may have to wait until tomorrow. I’ll do my best to get to this asap!

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 13, 2023, 4:26pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/9 "2023-02-13T16:26:07Z")

</div>

Awesome, Jeff! Thanks!

---

<div class="post-metadata">

**Author:** ![JeffAuriemma](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/jeffauriemma/32/2845_2.png) [@JeffAuriemma](https://community.apollographql.com/u/JeffAuriemma)\
**Post date:** [February 14, 2023, 3:38am UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/10 "2023-02-14T03:38:46Z")

</div>

Ok thanks for sharing, I’ve explored a few different possibilities but nothing seems conclusively wrong. A few follow-up questions:

1. In your video there was a `console.log` call that talked about a `read` function but I didn’t see that in the code you shared. What’s inside that `read` function? From the video it doesn’t appear that the cache is the problem but I’d like to rule out the possibility that there’s a buggy `read` function
2. Following that train of thought, it seems like the issue exists in the component code somewhere. It might be in your application code or it could be something counterintuitive that Apollo Client is doing. In your video you have logs with the text `number of objects in component` - where in your code was that being logged and what was the variable you were logging? Was it `actualObjects.length`?
3. Can you try dropping a `console.log` inside the `if (user?.realId && !objectsData) {` conditional in your `useEffect` call in `ObjectInventory`? Is that conditional being reached more frequently than you expect?

Thanks @nico!

---

<div class="post-metadata">

**Author:** ![nico](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/nico/32/3822_2.png) [@nico](https://community.apollographql.com/u/nico)\
**Post date:** [February 14, 2023, 7:34pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/11 "2023-02-14T19:34:12Z")

</div>

Hi again Jeff, will be glad to answer your questions:

1. I’ve removed the read function as I was only using for logging. I was just console.logging the params and returning them.
2. Indeed it was actualObjects.length
3. That was my first instinct as well, but I found out that that conditional was only being reached on first mount and never again.

---

<div class="post-metadata">

**Author:** ![JeffAuriemma](https://sea1.discourse-cdn.com/flex019/user_avatar/community.apollographql.com/jeffauriemma/32/2845_2.png) [@JeffAuriemma](https://community.apollographql.com/u/JeffAuriemma)\
**Post date:** [February 16, 2023, 6:24pm UTC](https://community.apollographql.com/t/apollo-client-shows-stale-data-after-leaving-current-screen/5633/12 "2023-02-16T18:24:45Z")

</div>

Thanks @nico. Okay I have one more avenue to try. I wonder whether you might be able to `console.log` the following, maybe after you declare `actualObjects`:

```typescript
  const [
    getInventoryObjects,
    { client, data: objectsData, loading: objectsLoading, error: objectsError, fetchMore: fetchMoreObjects },
  ] = useGetInventoryObjectsLazyQuery({
    notifyOnNetworkStatusChange: true,
  })

  const actualObjects = objectsData?.objects.items
  console.log(client.readQuery({
    query: GET_INVENTORY_OBJECTS_QUERY,
    // include all relevant variables, etc.
  }));

```

If we’re able to get a positive result from that `readQuery` call then I think we might need to take a look at whether there’s a bug or documentation gap for `useLazyQuery` that we need to address.
