react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Changing onViewableItemsChanged on the fly is not supported

Open zerocsss opened this issue 3 years ago • 36 comments

Please provide all the information requested. Issues that do not follow this format are likely to stall.

Description

Changing onViewableItemsChanged on the fly is not supported

React Native version:

0.63.3

Snack, code example, screenshot, or link to a repository:

<FlatList ref={FlatListRef} data={data} inverted={true} contentContainerStyle={{ flexGrow: 1, justifyContent: 'flex-end', }} renderItem={renderItem} keyExtractor={item => item.id} viewabilityConfig={{waitForInteraction: true, viewAreaCoveragePercentThreshold: 95}} onViewableItemsChanged={(info) => { console.log(info) }} />

image

zerocsss avatar Oct 14 '20 10:10 zerocsss

@zerocsss I recommend you to use this technique: First of all change property to pass this callback

-onViewableItemsChanged={...}
+viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}

where viewabilityConfigCallbackPairs is:

const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])

That's it.

isnifer avatar Oct 18 '20 11:10 isnifer

@zerocsss I recommend you to use this technique: First of all change property to pass this callback

-onViewableItemsChanged={...}
+viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}

where viewabilityConfigCallbackPairs is:

const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])

That's it.

Thanks it works like a charm

adblanc avatar Nov 12 '20 15:11 adblanc

@zerocsss could you please explain it, It didn't work for me

arbaz-yousuf-jazsoft avatar Dec 23 '20 18:12 arbaz-yousuf-jazsoft

@zerocsss could you please explain it, It didn't work for me

same here not working

rajeshivn avatar Mar 29 '21 05:03 rajeshivn

you also could wrap it with useCallback if it hasn't any deps (useCallback(() => {...} , []))

Nikolay-Vovk-dataart avatar Apr 09 '21 09:04 Nikolay-Vovk-dataart

@arbaz-yousuf-jazsoft @rajeshivn or anyone still trying to implement the solution above:

Add this to the top of your functional component:

const onViewableItemsChanged = ({
  viewableItems,
}) => {
  // Do stuff
};
const viewabilityConfigCallbackPairs = useRef([
  { onViewableItemsChanged },
]);

then render your Flatlist:

<FlatList
  style={styles.list}
  data={data}
  renderItem={renderItem}
  keyExtractor={(_, index) => `list_item${index}`}
  viewabilityConfigCallbackPairs={
    viewabilityConfigCallbackPairs.current
  }
/>

Thanks @isnifer this worked great!

johnhaup avatar Apr 16 '21 00:04 johnhaup

  viewabilityConfigCallbackPairs={
    viewabilityConfigCallbackPairs.current
  }

This method still has no response in my project. Although no error is reported, onViewableItemsChanged will not be executed.

omitchen avatar May 12 '21 03:05 omitchen

  viewabilityConfigCallbackPairs={
    viewabilityConfigCallbackPairs.current
  }

This method still has no response in my project. Although no error is reported, onViewableItemsChanged will not be executed.

@omitchen I had the same problem, my issue was that I had changed the name of the function onViewableItemsChanged in

const viewabilityConfigCallbackPairs = useRef([ { onViewableItemsChanged }, ]);

If you want to use a function with a different name, make sure to set it as the onViewableItemsChanged property in the useRef, as follows

const viewabilityConfigCallbackPairs = useRef([
  { onViewableItemsChanged: MyGreatFunction },
]);

julienripet avatar Jun 23 '21 08:06 julienripet

@zerocsss I recommend you to use this technique: First of all change property to pass this callback

-onViewableItemsChanged={...}
+viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}

where viewabilityConfigCallbackPairs is:

const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])

That's it.

This worked for me. Thanks!!

sobhanbera avatar Aug 06 '21 15:08 sobhanbera

Convert component from functional to class and call these methods, this will work like a charm

sallarahmed avatar Oct 04 '21 14:10 sallarahmed

@johnhaup i doing same thing in class component but it giving me error can't find variable onviewablechangeitem but i defined it

FoolCoder avatar Nov 24 '21 10:11 FoolCoder

@sallarahmed can u share class component code? i need it

FoolCoder avatar Nov 24 '21 10:11 FoolCoder

tried viewabilityConfigCallbackPairs in the function component, But getting Changing onViewableItemsChanged on the fly is not supported Error and Invariant Violation: Changing onViewableItemsChanged on the fly is not supported. Below is my code

<FlatList
ref={homeFeedRef}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={FlatListItemSeparator}
data={
home.nuggets
}
style={{ flex: 1 }}

viewabilityConfigCallbackPairs={[
{
viewabilityConfig: {
itemVisiblePercentThreshold: 100,
},
onViewableItemsChanged: handleChangeVIew,
},
{
viewabilityConfig: {
itemVisiblePercentThreshold: 200,
},
onViewableItemsChanged: handleChangeVIew2,
},
]}
ListHeaderComponent={topSection}
renderItem={renderItem} />

Also tried the below code, this does not throw any error but function call is not happening. Help needed

const viewabilityConfigCallbackPairs = useRef([ { viewabilityConfig, onViewableItemsChanged: handleChangeVIew }, ]);

prasanthvenkatachalam avatar Dec 10 '21 10:12 prasanthvenkatachalam

@isnifer tried viewabilityConfigCallbackPairs in the function component, that error got resolved but in the onViewableItemsChanged function I'm using a prop whose value is getting changed but inside the function, I'm not getting that prop new value, it's always giving me the same value which is coming for it the first time

        const indexOfHighlightedProduct = viewableItems.findIndex(({ item }) => item?.styleId === inFocusProduct)
        console.log('inFocusProduct', inFocusProduct)

        // if it is currently visible don't animate else animate
        if (indexOfHighlightedProduct > -1) {
            console.log('don"t do something')
        } else {
            console.log('do something')
        }
    }

Here inFocusProduct is the prop whose value is getting but it's not showing the changed value

abhilakshyadobhal avatar Jun 01 '22 10:06 abhilakshyadobhal

@abhilakshyadobhal Not sure you're doing animation decisions in the right place. I think component itself should do this. Current your behaviour is expected since useRef is not "reactive" hook

isnifer avatar Jun 01 '22 10:06 isnifer

working:

const viewabilityConfig = {
  waitForInteraction: true,
  viewAreaCoveragePercentThreshold: ITEM_HEIGHT
}

const handleViewableItemsChanged = useCallback((info) => {
  console.log('info', info)
}, []);

<FlatList
  ...
  viewabilityConfig={viewabilityConfig}
  onViewableItemsChanged={handleViewableItemsChanged}
/>

showtan001 avatar Jun 29 '22 07:06 showtan001

@showtan001 until handleViewableItemsChanged started to have some deps which will trigger a change of the function

isnifer avatar Jul 01 '22 09:07 isnifer

@abhilakshyadobhal I believe you have a stale closure problem. I'm not totally sure because you haven't provided your whole code. Does your code look something like this?

const onViewableItemsChanged = useCallback(info => {
  // your code from above
}, []);

Your prop inFocusProduct initial value was captured by the onViewableItemsChanged function. It doesn't matter how often the onViewableItemsChanged function is called, your console.log will always print the initial value. This article explains pretty well what stale closures are and how these can be addressed in React projects.

You most likely must use React state with FlatList's onViewableItemsChanged to prevent such situations. I run in similar situations with my React Native project. Approach 1 (using the state variable alreadySeen directly with an empty dependency array of useCallback) has the same stale state problem. Approach 2 (using the updater function setAlreadySeen with the callback function) fixed it for me.

Take a look at my example

// approach 1
const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
      const visibleItems = info.changed.filter((entry) => entry.isViewable);
      // perform side effect

      console.log("alreadySeen", alreadySeen);
      visibleItems.forEach((visible) => {
        const exists = alreadySeen.find((prev) => visible.item.name in prev);
        if (!exists) trackItem(visible.item);
      });
      // calculate new state
      setAlreadySeen([
        ...alreadySeen,
        ...visibleItems.map((visible) => ({
          [visible.item.name]: visible.item,
        })),
      ]);
    },
    []
  );

Here is Chrome's console output.

image

Pay attention to the alreadySeen output, it's always an empty array (this is the initial value of the useState call).

By the way, in my project, I use the React Hooks ESLint plugin and it yells at me that I have to add the alreadySeen dependency to the useCallback dependency array. This would solve the problem since React would then create a new function and would capture a new value of alreadySeen.

However, this is not possible with FlatList because you get the aforementioned "on the fly is not supported" error.

I solved it with approach 2 which leverages the state updater with a callback function.

// approach 2
const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
      const visibleItems = info.changed.filter((entry) => entry.isViewable);

      // this fixes the stale closure / omitted dependency problem
      setAlreadySeen((prevState: SeenItem[]) => {
        console.log("alreadySeen", prevState);
        // perform side effect
        visibleItems.forEach((visible) => {
          const exists = prevState.find((prev) => visible.item.name in prev);
          if (!exists) trackItem(visible.item);
        });
        // calculate new state
        return [
          ...prevState,
          ...visibleItems.map((visible) => ({
            [visible.item.name]: visible.item,
          })),
        ];
      });
    }, []
  );

This approach solves the problem: setAlreadySeen((prevState: SeenItem[]) => { /* ... /* })

As you can see, the console output is now correct and the alreadySeen array gets updated correctly.

image

doppelmutzi avatar Jul 01 '22 20:07 doppelmutzi

Is there any way to just get the value of a state and use it in the onViewableItemsChanged ?

I actually have this :

  const onViewableItemsChanged = useCallback(
    items => {
      console.log('value : ', mode);
    },
    [mode]
  );

As soon as It renders, I got the onViewableItemsChanged on the fly is not supported, the only way I found to deal with it is to set key={mode} on my flatlist but when my mode switchs it scrolls on the top of the flatlist, and if i save with ctrl + S, the hot reload makes it crash with the same error as before.

I hadn't any crash when I tried useRef but I hadn't my state value updated just got the initial state

I don't need to do any state update inside my onViewableItemsChanged just to access the state value update

Estro-1 avatar Aug 02 '22 10:08 Estro-1

@Estroo1 the way I got around that was to create useRef values that mapped to my state values (use a useEffect to update the useRef values on a state change). Then use the ref values within onViewableItemsChanged

Note: I can confirm that this works when the onViewableItemsChanged is also a useRef, not sure about when its wrapped in useCallback

sweatherall avatar Aug 03 '22 21:08 sweatherall

For me, the issue kept happening because i passed onViewableItemsChanged an anonymous function.

I needed to have both a scroll and button work and update a pagination component when user uses either or.

Here's the key points:

Flatlist Enable paginEnabled, onViewableItemsChanged used but without passing an anonymous function, viewabilityConfig itemVisiblePercentThreshold set to 100

<FlatList
  ref={flatListRef}
  horizontal
  pagingEnabled
  data={data}
  renderItem={renderItem}
  onViewableItemsChanged={onScroll} // Caliing with anonymous function here causes a bug
  viewabilityConfig={{
    itemVisiblePercentThreshold: 100,
  }}
/>

Button

Using the flatListRef and my useState, to scroll to the next index and keep track of where the user is at.

const flatListRef = useRef<FlatList>(null); const [currentSectionIndex, setCurrentSectionIndex] = useState(0);

  const onButtonPress = useCallback(() => {
    if (currentSectionIndex === data.length - 1) {
      // What to do at the end of the list
    } else if (flatListRef.current) {
      flatListRef.current.scrollToIndex({
        index: currentSectionIndex + 1,
      });
      setCurrentSectionIndex(currentSectionIndex + 1);
    }
  }, [currentSectionIndex, navigation, data.length]);

OnScroll function I didn't know i could catch the viewableItems param without using an anonymous function, but you can. Here i am using it to grab the first item in the list, and setting the useState to it's index to update pagination.

// When scrolling set useState to render pagination correctly
  const onScroll = useCallback(({ viewableItems }) => {
    if (viewableItems.length === 1) {
      setCurrentSectionIndex(viewableItems[0].index);
    }
  }, []);

PhillipFraThailand avatar Aug 23 '22 07:08 PhillipFraThailand

@Estroo1 Didn't you see my post? There you can see that you can achieve exactly that with the state setter (which gets the previous state as an argument). It is directly the post before yours.

doppelmutzi avatar Sep 04 '22 13:09 doppelmutzi

@Estroo1 Didn't you see my post? There you can see that you can achieve exactly that with the state setter (which gets the previous state as an argument). It is directly the post before yours.

In your case you're setting the state value inside the onViewableItemsChanged, in my case I needed to get the state value, unfortunately I keep getting the initial value and not the updated one. Basically my setter and my onViewableItemsChanged are not related but I need to get the value and it doesn't work as wanted

Estro-1 avatar Sep 04 '22 14:09 Estro-1

But you can also use this approach to just read the state (and do something with it) and ignore to set the state. Just return the same value or do not return anything at all. With this approach, you do not have the problem with your stale closure problem (i.e., that you have your initial value all the time).

doppelmutzi avatar Sep 04 '22 14:09 doppelmutzi

Yeah I tried to implement it before posting but it wasn't successful even with your method, maybe I missed something. I should take a look one day to understand it better

Estro-1 avatar Sep 04 '22 20:09 Estro-1

it's been 2 years

imfunniee avatar Sep 20 '22 17:09 imfunniee

If you are using a different method then onViewableItemsChanged make sure it is defined above the useRef lines. Like this:

const onMyDataScroll = (changed,viewableItems) => { console.log(changed) }

const viewabilityConfig = { viewAreaCoveragePercentThreshold: 95 } const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig: viewabilityConfig, onViewableItemsChanged: onMyDataScroll }])

saurav1124 avatar Oct 12 '22 15:10 saurav1124