Open zerocsss opened 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.
@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
@zerocsss could you please explain it, It didn't work for me
@zerocsss could you please explain it, It didn't work for me
same here not working
you also could wrap it with useCallback
if it hasn't any deps (useCallback(() => {...} , [])
)
@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!
viewabilityConfigCallbackPairs={ viewabilityConfigCallbackPairs.current }
This method still has no response in my project. Although no error is reported, onViewableItemsChanged will not be executed.
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 },
]);
@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!!
Convert component from functional to class and call these methods, this will work like a charm
@johnhaup i doing same thing in class component but it giving me error can't find variable onviewablechangeitem but i defined it
@sallarahmed can u share class component code? i need it
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 }, ]);
@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 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
working:
const viewabilityConfig = {
waitForInteraction: true,
viewAreaCoveragePercentThreshold: ITEM_HEIGHT
}
const handleViewableItemsChanged = useCallback((info) => {
console.log('info', info)
}, []);
<FlatList
...
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={handleViewableItemsChanged}
/>
@showtan001 until handleViewableItemsChanged
started to have some deps which will trigger a change of the function
@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.
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.
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
@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
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);
}
}, []);
@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.
@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
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).
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
it's been 2 years
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 }])
@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
const onViewableItemsChanged = ({ viewableItems, }) => { // Do stuff }; const viewabilityConfigCallbackPairs = useRef([ { onViewableItemsChanged }, ]);
reply : Changing viewabilityConfigCallbackPairs on the fly is not supported
even with the above solution, I also am facing this "Changing viewabilityConfigCallbackPairs on the fly is not supported" unfortunately
@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/
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
/>
who know how fixed this in RN 71?
@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
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;
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.
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?
Changing onViewableItemsChanged on the fly is not supported
this one works for react native 0.71.1
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.
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} /> );`
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
}
RN 0.73 makes this a bit better: https://github.com/facebook/react-native/commit/5cfa125b979c7ce76884a81dd3baaddcf4a560fd
hopefully the new architecture solves this
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) }} />