facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.18k stars 24.32k forks source link

Changing onViewableItemsChanged on the fly is not supported #30171

Open zerocsss opened 4 years ago

zerocsss commented 4 years ago

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

isnifer commented 4 years ago

@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.

adblanc commented 3 years ago

@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

arbaz-yousuf-jazsoft commented 3 years ago

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

rajeshivn commented 3 years ago

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

same here not working

Nikolay-Vovk-dataart commented 3 years ago

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

johnhaup commented 3 years ago

@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!

omitchen commented 3 years ago
  viewabilityConfigCallbackPairs={
    viewabilityConfigCallbackPairs.current
  }

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

julienripet commented 3 years ago
  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 },
]);
sobhanbera commented 3 years ago

@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!!

sallarahmed commented 3 years ago

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

Qazafi-Dev commented 2 years ago

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

Qazafi-Dev commented 2 years ago

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

prasanthvenkatachalam commented 2 years ago

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 }, ]);

abhilakshyadobhal commented 2 years ago

@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

isnifer commented 2 years ago

@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

showtan001 commented 2 years ago

working:

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

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

<FlatList
  ...
  viewabilityConfig={viewabilityConfig}
  onViewableItemsChanged={handleViewableItemsChanged}
/>
isnifer commented 2 years ago

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

doppelmutzi commented 2 years ago

@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
Estroo commented 2 years ago

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

sweatherall commented 2 years ago

@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

PhillipFraThailand commented 2 years ago

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);
    }
  }, []);
doppelmutzi commented 2 years ago

@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.

Estroo commented 2 years ago

@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

doppelmutzi commented 2 years ago

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).

Estroo commented 2 years ago

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

imfunniee commented 2 years ago

it's been 2 years

saurav1124 commented 2 years ago

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 }])

nickdev21 commented 1 year ago

@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.

Yup this one is really helpful

anazrasak commented 1 year ago
const onViewableItemsChanged = ({
  viewableItems,
}) => {
  // Do stuff
};
const viewabilityConfigCallbackPairs = useRef([
  { onViewableItemsChanged },
]);

reply : Changing viewabilityConfigCallbackPairs on the fly is not supported

pierroo commented 1 year ago

even with the above solution, I also am facing this "Changing viewabilityConfigCallbackPairs on the fly is not supported" unfortunately

doppelmutzi commented 1 year ago

@pierroo I wrote an article about this topic. Maybe, it helps you. Again, you have to understand the limitations of this onViewableItemsChanged prop and how you can tackle this with React lifecycle techniques. And this is where this state setter function comes into play.

https://blog.logrocket.com/implementing-component-visibility-sensor-react-native/

AHMED-5G commented 1 year ago

this solution working with me

  const onViewableItemsChanged = ({ viewableItems }) => {

    console.log(viewableItems)
  };

  const viewabilityConfig = { itemVisiblePercentThreshold: 100 };

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

<FlatList

...
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}

//viewabilityConfig={{ itemVisiblePercentThreshold: 100,  }}    <-------------------------remove this 

        />
SlavaC7 commented 1 year ago

who know how fixed this in RN 71?

evo-sampath commented 1 year ago

@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.

onViewableItemsChanged function is not triggering in this case

WiharlleyWill commented 1 year ago

on FlatList use:

viewabilityConfig={viewabilityConfig} onViewableItemsChanged={onViewableItemsChanged}

const viewabilityConfig = {
        waitForInteraction: true,
        itemVisiblePercentThreshold: 50 //Check the price that best suits your needs.
    }
const onViewableItemsChanged = useRef(({ viewableItems }) => {

    try {
        console.log(viewableItems)
        //do anything
    } catch (error) {
        console.log(error);
    }
}).current;
Ksiner commented 1 year ago

In my case it was that I had multiple FlatLists mounted (on different screens that were mounted at the same time, using BottomTabNavigator from @react-navigation/bottom-tabs). So I've specified the key prop on the list and the problem disappeared:

<FlatList
  key="posts"
  viewabilityConfig={viewabilityConfig} // useMemo
  onViewableItemsChanged={onViewableItemsChanged} // useCallback or useRef or any approach you like from comments above
<Flatlist />

So in my case, the issue was probably that the FlatList confused its props with other FlatLists. That's really odd behavior, as those lists are placed in different component tree structures (on different screens), yet somehow they lack the key that would distinguish them from one another.

AnandKuhar1100 commented 1 year ago

If we are using onViewableItemsChanged with useRef or useCallback then we are not getting updated redux values directly in onViewableItemsChanged. We need to use useRef with redux values as well. Is there any other approach for the same?

KhineKhineMyatNoe commented 1 year ago

this one works for react native 0.71.1

SirCameron commented 1 year ago

I'm not sure what the point of having a functioned defined in stone (useRef, useCallback with []) to receive the viewable item updates when it can't DO anything with that information - cannot update state or anything like that... it's pointless.

nguyentrancong commented 10 months ago

I've fixed it, and working with me, You see code belw

` const viewConfigRef = React.useRef({viewAreaCoveragePercentThreshold: 50});

const onViewCallBack = React.useCallback(async (info: any) => { const item = await head(info?.viewableItems); onChangePosition(item.index); }, []);

return ( <FlatList ref={flatListRef} data={questions} keyExtractor={item => item.id} renderItem={renderItemQuestion} style={styles.flatList} pagingEnabled horizontal showsHorizontalScrollIndicator={false} viewabilityConfig={viewConfigRef.current} onViewableItemsChanged={onViewCallBack} /> );`

gaearon commented 8 months ago

I tagged this as a bug but not sure when the RN team will be able to dedicate time to it.

In the meantime, a workaround like this should work:

import {useCallback, useInsertionEffect, useRef} from 'react'

function MyComponent() {
  const onViewableItemsChanged = useNonReactiveCallback(() => {
    // ... your code ...
  });

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

// Note: Use this sparingly.
function useNonReactiveCallback<T extends Function>(fn: T): T {
  const ref = useRef(fn)
  useInsertionEffect(() => {
    ref.current = fn
  }, [fn])
  return useCallback(
    (...args: any) => {
      const latestFn = ref.current
      return latestFn(...args)
    },
    [ref],
  ) as unknown as T
}
NickGerleman commented 8 months ago

RN 0.73 makes this a bit better: https://github.com/facebook/react-native/commit/5cfa125b979c7ce76884a81dd3baaddcf4a560fd

imfunniee commented 7 months ago

hopefully the new architecture solves this