recyclerlistview
recyclerlistview copied to clipboard
Warn: Possible stableId collision
**VirtualRenderer.prototype.syncAndGetKey **
In line if (stackItem && stackItem.dataIndex !== index) {...}
Sometimes the key is equal to type ( 0 for me ), How do I fix this warning?
Returning the same stableId for two different items? Any repro steps or expo snack?
@naqvitalha In that and the fact that I get a list of messages from the server and there are no duplicate IDs, there is a suspicion that the place of the key he sometimes puts not stableid but the element type...
Can you share you data provider and row renderer if possible? Also make sure stableId is a string
I also sometimes get this even if i do not add getStableId
argument to my dataprovider (in which case it uses the indexes as the ids: ref).
hey! i think i have 'something' about this issue. context: i'm on react-native using RLV latest beta. the issue arises when we're updating the dataset, adding new elements to the 'head', i mean: ['id_10', 'id_9', 'id_8', 'id_7', ...] => ['id_11', 'id_10', 'id_9', 'id_8', 'id_7', ...].
i think the problem is within VirtualRenderer
's handleDataSetChange
method. i'm kinda debugging using old plain console.log. at the beginning of the mentioned method this._renderStack
is:
{ id_10: { dataIndex: 0 },
id_9: { dataIndex: 1 },
id_8: { dataIndex: 2 },
id_7: { dataIndex: 3 },
id_6: { dataIndex: 4 },
id_5: { dataIndex: 5 },
id_4: { dataIndex: 6 },
id_3: { dataIndex: 7 },
id_2: { dataIndex: 8 },
id_1: { dataIndex: 9 } }
when you assign newRenderStack
to this._renderStack
its value is:
{ id_1: { dataIndex: 0 },
id_10: { dataIndex: 1 },
id_9: { dataIndex: 2 },
id_8: { dataIndex: 3 },
id_7: { dataIndex: 4 },
id_6: { dataIndex: 5 },
id_5: { dataIndex: 6 },
id_4: { dataIndex: 7 },
id_3: { dataIndex: 8 },
id_2: { dataIndex: 9 } }
maybe i'm wrong -so forgive me in that case!-, but:
- should
id_1
value be{ dataIndex: 10 }
instead? - should
newRenderStack
have the new property-value:id_11: { dataIndex: 0 }
?
hope this helps!
hey @naqvitalha , i've solved this :) if you have time you can check this out: https://github.com/iamyellow/recyclerlistview/commit/babd9a66b336796b875ead96770a63fbb80506a4
if you're opened to PR, it'll be awesome if i can contribute.
also FYI, if you check my fork branch you'll see i've added support for custom row / cell component holder since i'm working with the amazing react-native-reanimated library and -as it happens w/ other animation frameworks- you need to use a custom Animated.View
. I'd do the PR, but a) I prefer to ask first the author just in case he/she doesn't like my eg naming
, code style, etc. and b) i made available the holder for RN, so web version is missing since I have no experience w/ reactjs
please, let me know your thoughts. and enjoy your day!
Sadly, but the feeling that the project was abandoned (((
Animations do not work correctly, because of replacing the appearance of the block occurs not from the bottom up, and first from the top Inthey and then immediately from the bottom up, because the new element uses the newly vacant cell
@legion-zver
honestly i don't think it's abandoned, actually you can see recent activity in at least another branch. that being said, the core feature works really well, and i understand that was the main priority. animations and performance fine tuning is happening (and it's still in beta).
about animation, i'm not understanding you, sorry. i'm developing a im interface and, using reanimated as i mentioned, and after understanding the logic behind dataset updates i'm getting very good results :)
bad news... the fix was not a fix... as soon as you scroll up, then down, then change the dataset the warn is still there... and i'm not able to find time to understand what is happening. one thing is clear btw: the problem arises whenever you change the dataset 'moving' items, like shifting an array. with the simple chat example: whenever you receive new messages. it works ok if you add past messages. @naqvitalha could you please try to explain some core concepts (like the render stack, etc.) in order to let me take a deeper look to this?
One thing I can think of is that you might be mutating the same array that you passed to DataProvider. Ideally according to immutability principle you shouldn't. Try cloning instead and see. If it doesn't help try creating a repro on expo and I will look into it ASAP.
And the project is very much active. We just released two huge projects on top of this. Beta is much more active :)
actually i'm using immer for immutability, passing the new state array to cloneWithRows, so i think that's not the problem. i'll tell you more: i've tried to assign a new getStableId to the new DataProvided returned by the mentioned method and i've could check that it's trying to get stable ids from indexes that didn't exist in the previous state. eg first state is an empty dataset, then 10 items, you can see how it's trying to get stable ids from empty dataset!
anyway, i'll try to make an expo hello world trying to reproduce the problem
morning! expo is taking ages to publish, so as it's just 3 files i've made a gist: https://gist.github.com/iamyellow/5259fef2f71ef4f8308e3c45880055ae
i'm using the sample component from RLV guide, with a few changes:
- for the sake of simplicity, just one layout with large yellow items
- added the getStableId callback, obviously. it returns the data[i] value as stable id
- if you press any item, it'll prepend a new row with an id > previous item, it's quite a simple logic... you'll see!
in order to reproduce the problem:
- open the app
- tap any item FIRST
- scroll down a bit and there it is: "possible stable collision @ 6"
BUT, eg
- open the app
- scroll down like 15 items
- scroll to the top
- tap an item and boom!
there's more random situation where it seems to works, but then you scroll or something and there it is again.
what do you thing @naqvitalha ? thanks!
@naqvitalha I'm also having this same warning. The way I add more data to the data provider is by querying a internal db. Every time I load more data, I query the db with a new limit, so the array is always whatever the query returns. So the new data array is always a new one.
The weird thing is in my case only happens with the first rows and doesn't happen all the time.
What I did notice is that when I first load the list, the first key on the renderStack is at dataIndex: 0, after a few scrolls loading more data, that same key ends up in the dataIndex: 35 and then throws the warning saying that key already exists on the renderStack
yes @tafelito, it seems the same bug. i also traced the render stack and noticed the same dataIndex stuff.
I'm investigating. Let me get back.
nice @naqvitalha!
So, I've figured out the issue. Consider the following example:
Stable Ids (Scenario 1):
1, 2, 3, 4, 5, 6, 7, 8, 9
Visible Indices: 1 to 7
Post Data Change (1 item got inserted):
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Visible Indices: 0 to 6
For perf reasons we don't scan full data on every change. Here RLV thinks that index 7 is being discarded so, it utilises it to render the new id 0. So, the insertion will not require any mounts. However, now when you scroll down there is no item in recycle pool and 7 isn't available for rendering. RLV thinks of it as an anomaly and creates a new recycling key using an internal logic. Now this is perfectly fine and, certainly, is an optimal way to deal with the change. The warning is incorrect though. There is nothing to worry about. It can be removed I think.
You can also enable optimizeForInsertDeleteAnimations={true}
to prevent RLV from being too aggressive about reuse.
@naqvitalha When to wait for the new version of Beta?
Closing as this has been fixed in the latest release.
Hey guys, I have same issue. Installed latest Beta version and also tested on stable. I created chat app, I load dataset from DB then when you send new message, it loads new bunch of messages and replace current state, so I my total message count do not change. I have tried different solutions, unfortunately it is not working as expected. Also additional error is throw Encountered two children with same key. As I see, then for some reason LayoutProvider is calling 0 index multiple times, hence I assume it is rendering zero index multiple times. Not clear what is the issue here. Does anybody have working example for chat app with stable id?
I'm having this issue, only when I specify the stableId
argument when creating a data provider.
const dataProvider = useMemo(
() =>
new DataProvider(
(item1: Hit, item2: Hit) => item1.objectID !== item2.objectID,
(index) => hits?.[index]?.objectID // this causes the error
).cloneWithRows(hits ?? []),
[hits]
)
I can confirm with certainty that my list does not have any duplicates, nor am I mutating it at any time. This is my full code:
// ...imports
export default function FastArtistHitsCards(props: Props) {
const { hits, fetchMore } = props
const { width, height, onLayout } = useLayout()
let itemsPerRow = 2
if (width >= defaultBreakpoints[1]) {
itemsPerRow = 3
}
const itemWidth = width / itemsPerRow
const itemHeight = itemWidth * 1.5
const dataProvider = useMemo(
() =>
new DataProvider(
(item1: Hit, item2: Hit) =>
// define that two items are not the same
item1.objectID !== item2.objectID,
// get the stable id for a given item
(index) => hits?.[index]?.objectID
).cloneWithRows(hits ?? []),
[hits]
)
const layoutProvider = useMemo(() => {
return new LayoutProvider(
(index) => {
return 'card' // every item is the same
},
(_, dimensions) => {
dimensions.width = itemWidth
dimensions.height = itemHeight
}
)
}, [itemHeight, itemWidth])
// we need to re-render if the width changes
const extendedState = useMemo(() => ({ width }), [width])
const renderList = () => {
if (!width || !hits?.length) return <LoadingScreen />
return (
<RecyclerListView
dataProvider={dataProvider}
layoutProvider={layoutProvider}
extendedState={extendedState}
rowRenderer={(_, artist, index) => {
return (
<ArtistCardFixedSize
artist={artist}
height={itemHeight}
width={itemWidth}
/>
)
}}
style={{
height,
width,
}}
scrollViewProps={{
showsVerticalScrollIndicator: false,
keyboardDismissMode: 'on-drag',
keyboardShouldPersistTaps: 'handled',
}}
onEndReached={fetchMore}
/>
)
}
return (
<View sx={{ flex: 1 }} onLayout={onLayout}>
{renderList()}
</View>
)
}
I'm a bit confused as to why this is happening. I am certain that the ID I'm returning is unique in the list. I really need to use this field, because it seems as though the recycling is causing my images to flicker. When I first scroll to an item that is far down, it shows up as the wrong image's index, and then it changes to the correct one. I assume this is because of the unique keys being wrong.
I think I identified the issue: DataProvider
doesn't change on re-renders. My data in the example above was called hits
. It turns out, DataProvider
is using a stale hits
array.
const dataProvider = useMemo(
() =>
new DataProvider(
(item1: Hit, item2: Hit) => item1.objectID !== item2.objectID,
(index) => hits?.[index]?.objectID // this causes the error
).cloneWithRows(hits ?? []),
[hits]
)
Even when I infinite scroll and get new hits, it is still referencing the original hits object.
I even tried to move this into a ref, so that it always accesses the latest hits
array: '
const hitsRef = useRef(hits)
useEffect(() => {
hitsRef.current = hits
})
const dataProvider = useMemo(() => {
console.log({ hits }) // this logs correctly!
return new DataProvider(
(item1: Hit, item2: Hit) => item1.objectID !== item2.objectID,
(index) => {
// these only log correctly on the first render.
console.log({ hits, hitsRef })
hitsRef.current?.[index]?.objectID // changed here
}
).cloneWithRows(hits ?? [])
}, [hits])
What's weird is, in the stable ID function, items that do indeed exist in the hits
array are showing up as undefined. I really don't understand it. I am even logging the new hits
variable inside of useMemo
. It exists. But when I log the new one inside of the stableId
function, the updated hits variable no longer exists. My list size is changing from 20 to 40 to 60 as you scroll, so the hits
variable gets larger and larger. For some reason, inside of the stableId
function, it always stays at a length of 20.
@nandorojo did you end up finding a solution to this? I'm running into the same problem.
Closing as this has been fixed in the latest release.
@arunreddy10 perhaps you can reopen the issue? I'm on version 3.0.5
and this problem still exists.
No, I ended up not using recycler list. I tried for a while but could never get it to work. Found the API pretty confusing too. Wish I could help.
@nandorojo that's a pity, thank you for replying though!
Unfortunately the way to go seems to not use stable ids at all.
@lucasbento I was just able to work around this using a ref. I'm using a class component manually assigning like this:
// top of the class
latestParsedData: React.MutableRefObject<whatever | null>;
// in the constructor
this.latestParsedData = React.createRef();
// render()
this.latestParsedData.current = props.updatedData;
I'm not certain why @nandorojo's solution didn't work, but I suspect useEffect
needs the second param:
useEffect(() => {
hitsRef.current = hits
}, [hits])
@lucasbento I was just able to work around this using a ref. I'm using a class component manually assigning like this:
// top of the class latestParsedData: React.MutableRefObject<whatever | null>; // in the constructor this.latestParsedData = React.createRef(); // render() this.latestParsedData.current = props.updatedData;
I'm not certain why @nandorojo's solution didn't work, but I suspect
useEffect
needs the second param:useEffect(() => { hitsRef.current = hits }, [hits])
I did not got how first solution is working. Can you give a full sample code ? Thanks