question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

onViewableItemsChanged not firing if items change without scroll šŸ˜”

See original GitHub issue

I’ve seen a couple of issues mentioning onViewableItemsChanged not working but thought I should open a new one because they don’t provide reproducible examples and I found them a bit unclear

Current behavior

Adding new items to the FlashList does not cause the onViewableItemsChanged to be fired.

To Reproduce

Try tapping the button once loaded, the handler does not fire.

import { FlashList } from '@shopify/flash-list'
import React, { useState } from 'react'
import { Button, Text, ViewabilityConfig } from 'react-native'

const viewabilityConfig: ViewabilityConfig = {
  itemVisiblePercentThreshold: 0,
}

function handleViewableItemsChanged() {
  console.log('handleViewableItemsChanged')
}

export function ExampleFlashList() {
  const [data, setData] = useState(() => Array.from({ length: 100 }, (_, i) => i))
  return (
    <FlashList
      data={data}
      inverted
      ListHeaderComponent={
        <Button
          title="Add more items"
          onPress={() => {
            setData(x => [...Array.from({ length: 10 }, (_, i) => x[0] - (10 - i)), ...x])
          }}
        />
      }
      onViewableItemsChanged={handleViewableItemsChanged}
      renderItem={renderItem}
      estimatedItemSize={40}
      viewabilityConfig={viewabilityConfig}
      keyboardDismissMode="interactive"
      keyboardShouldPersistTaps="handled"
      onEndReachedThreshold={0.5}
    />
  )
}

function renderItem({ item }: { item: number }) {
  return <Text>Item: {item}</Text>
}

Platform:

  • iOS Android (untested)

Environment

1.2.2

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:3
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
bfrickacommented, Sep 27, 2022

@hirbod Not OP, but my setup implements:

  • disableAutoLayout
  • estimatedItemSize
  • estimatedListSize
  • getItemType
  • keyExtractor
  • numColumns
  • onBlankArea
  • onViewableItemsChanged
  • overrideItemLayout
  • viewabilityConfig (tried various tweaks here, and even calling flashRef.recordInteraction() imperatively).

Anyways, looking at the underlying code, it’s relatively clear that the issue for me is the fact that FlashList sort of conflates ā€œvisible indicesā€ with ā€œvisible itemsā€. Frequently, new items can be added, without the visible indices changing at all.

It’s very likely, therefore, that the reason I would occasionally see this fire, is because adding a new item would change the layout in such a way that visible indices changes (probably because I’m using numColumns).

Note that I have not verified this. I just looked at the code and the fact that FlashList relies on the underlying onVisibleIndicesChanged from recycler. I can go into it and figure out exactly what is going on, and probably fix it, if the maintainers are interested.

On my end, I already found a workaround, and that is keeping a reference to the visible indices. If something new comes in and it’s within the visible range, I make sure the extra data is loaded.

Looks something like:

type ViewRange = [startIdx: number, endIdx: number]
const DEFAULT_VIEW_RANGE: ViewRange = [0, 1]
// List
const viewRangeRef = useRef(DEFAULT_VIEW_RANGE)

const handleViewableItemsChanged = ({ viewableItems }: FooViewabilityInfo) => {
  viewRangeRef.current[0] = viewableItems[0]?.index || 0
  viewRangeRef.current[1] = viewableItems[viewableItems.length - 1]?.index || 1
}

Not ideal, but it’ll work until this is fixed.

0reactions
mfbx9da4commented, Oct 20, 2022

For my particular use case all I care about is if the first visible item changed I built a hook for this inspired from @bfricka’s comment. A similar strategy could be used to fully fix FlashList

import { FlashList, ViewToken } from '@shopify/flash-list'
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Button, Text, ViewabilityConfig } from 'react-native'


function isTruthy<T>(x: T): x is Exclude<T, Falsy> {
  return x !== null && x !== undefined && (x as any) !== false
}


const viewabilityConfig: ViewabilityConfig = {
  itemVisiblePercentThreshold: 50,
}

export function ExampleFlashList() {
  const [data, setData] = useState(() => Array.from({ length: 100 }, (_, i) => i))
  console.log('=================')
  const ref = useRef<FlashList<any>>(null)

  const notifyVisibleItemsChanged = useFirstVisibleItemChanged(data, item => {
    console.log('newly visible first item', item)
  })

  const handleViewableItemsChanged = useCallback(
    (info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => {
      notifyVisibleItemsChanged(info)
      console.log('handleViewableItemsChanged')
    },
    [notifyVisibleItemsChanged]
  )

  return (
    <FlashList
      ref={ref}
      data={data}
      inverted
      ListHeaderComponent={
        <Button
          title="Add more items"
          onPress={() => {
            setData(x => [...Array.from({ length: 10 }, (_, i) => x[0] - (10 - i)), ...x])
          }}
        />
      }
      onViewableItemsChanged={handleViewableItemsChanged}
      renderItem={renderItem}
      estimatedItemSize={40}
      viewabilityConfig={viewabilityConfig}
      keyboardDismissMode="interactive"
      keyboardShouldPersistTaps="handled"
      onEndReachedThreshold={0.5}
    />
  )
}

function useFirstVisibleItemChanged<T>(
  data: T[],
  onFirstVisibleItemChanged: (item: { item: T; index: number }) => void
) {
  const viewableIndicesRef = useRef<number[]>([])

  const callbackRef = useRef(onFirstVisibleItemChanged)
  callbackRef.current = onFirstVisibleItemChanged

  const firstVisibleItemRef = useRef<T | null>(null)
  const firstVisibleItem = data[viewableIndicesRef.current[0]]
  useLayoutEffect(() => {
    if (firstVisibleItem !== firstVisibleItemRef.current) {
      callbackRef.current({ item: firstVisibleItem, index: viewableIndicesRef.current[0] })
      firstVisibleItemRef.current = firstVisibleItem
    }
  }, [firstVisibleItem])

  const trackVisibleIndices = useCallback(
    (info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => {
      viewableIndicesRef.current = info.viewableItems.map(v => v.index).filter(isTruthy)
    },
    []
  )
  return trackVisibleIndices
}

function renderItem({ item }: { item: number }) {
  return <Text>Item: {item}</Text>
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

React Native "onViewableItemsChanged" not working while ...
I have a React Native FlatList. base on Documentation I used onViewableItemsChanged for getting the current showing item on the screenĀ ...
Read more >
onViewableItemsChanged is broken if pagingEnabled is true ...
Scroll the FlatList and see the log of viewableItems , viewableItems will log all the items when scroll to the first item, which...
Read more >
Understand onViewableItemsChanged in FlatList - RY 's Blog
onViewableItemsChanged is a prop in VirtualizedList and FlatList. When you scroll a FlatList, the items showing on the FlatList change.
Read more >
onViewableItemsChanged: check last item is visible on screen
Hi everybody, in my react-native app I'm using onViewableItemsChanged prop on FlatList in order to see if the last item of FlatList isĀ ......
Read more >
React Native scrollToIndex - Dynamic size item scroll inside ...
In this video tutorial you will learn about React Native scrollToIndex and how to scroll to an item inside a FlatList, ListView, ScrollView,Ā ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found