Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn: Possible stableId collision #201

Open
legion-zver opened this issue Jun 16, 2018 · 52 comments
Open

Warn: Possible stableId collision #201

legion-zver opened this issue Jun 16, 2018 · 52 comments

Comments

@legion-zver
Copy link

**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?

@naqvitalha
Copy link
Collaborator

Returning the same stableId for two different items? Any repro steps or expo snack?

@legion-zver
Copy link
Author

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

@naqvitalha
Copy link
Collaborator

Can you share you data provider and row renderer if possible? Also make sure stableId is a string

@Jyrno42
Copy link

Jyrno42 commented Jul 11, 2018

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

@iamyellow
Copy link

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!

@iamyellow
Copy link

iamyellow commented Sep 13, 2018

hey @naqvitalha , i've solved this :) if you have time you can check this out:
iamyellow@babd9a6

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!

@legion-zver
Copy link
Author

Sadly, but the feeling that the project was abandoned (((

@legion-zver
Copy link
Author

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

@iamyellow
Copy link

@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 :)

@iamyellow
Copy link

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?

@naqvitalha
Copy link
Collaborator

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.

@naqvitalha
Copy link
Collaborator

And the project is very much active. We just released two huge projects on top of this. Beta is much more active :)

@iamyellow
Copy link

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!

@iamyellow
Copy link

anyway, i'll try to make an expo hello world trying to reproduce the problem

@iamyellow
Copy link

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!

@tafelito
Copy link
Contributor

tafelito commented Sep 14, 2018

@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

@iamyellow
Copy link

yes @tafelito, it seems the same bug. i also traced the render stack and noticed the same dataIndex stuff.

@naqvitalha
Copy link
Collaborator

I'm investigating. Let me get back.

@iamyellow
Copy link

nice @naqvitalha!

@naqvitalha
Copy link
Collaborator

naqvitalha commented Sep 17, 2018

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.

@legion-zver
Copy link
Author

@naqvitalha When to wait for the new version of Beta?

@arunreddy10
Copy link
Collaborator

Closing as this has been fixed in the latest release.

@davx1992
Copy link

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?

@nandorojo
Copy link

nandorojo commented Oct 31, 2020

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.

@nandorojo
Copy link

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.

@lucasbento
Copy link

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

@nandorojo
Copy link

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.

@lucasbento
Copy link

@nandorojo that's a pity, thank you for replying though!

Unfortunately the way to go seems to not use stable ids at all.

@daniel-centore
Copy link

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

@siddharth-kt
Copy link

@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

@daniel-centore
Copy link

I did not got how first solution is working. Can you give a full sample code ? Thanks

I did this here: https://github.com/daniel-centore/WikiSpiv-App/blob/680e5808b67788b6e2c8b28b1460c8e4f7d3cfe8/components/FlatListQuickScroll/FlatListQuickScroll.tsx

@siddharth-kt
Copy link

@daniel-centore Thanks

@siddharth-kt
Copy link

siddharth-kt commented Feb 26, 2022

@daniel-centore @davx1992
I need your help, On removing any item from dataProvider, I am facing this issue.

Warning: Encountered two children with the same key, 181. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

Can you please tell any solution ?

@siddharth-kt
Copy link

@daniel-centore ?

@daniel-centore
Copy link

@siddharth-kt I am not a recyclerlistview dev nor am I an expert in it, just someone who has used it before. You are welcome to look at the code I linked to above for a working example. If you need debug support you should probably include a minimum example demonstrating the issue. I cannot promise I'll have time to look at it, but it would be useful for others too.

@naqvitalha naqvitalha reopened this Mar 10, 2022
@naqvitalha
Copy link
Collaborator

Folks, I'm looking into this. Will update the issue soon.

@siddharth-kt
Copy link

siddharth-kt commented Mar 10, 2022

Thanks @daniel-centore

Hi @naqvitalha thanks for looking into this,
I am waiting for the solution ...

@daniel-centore @davx1992 I need your help, On removing any item from dataProvider, I am facing this issue.

Warning: Encountered two children with the same key, 181. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

Can you please tell any solution ?

kindly see this issue.

Example code :
(I am not doing anything special, but when ever I delete/remove multiple cells/objects from dataProvider, then it gives me above mentioned warning and after warning cells/objects in my RecyclerListView overlap each-other, thus its huge issue).

<RecyclerListView
ref={this._setRef}
dataProvider={this.state.dataProvider}
layoutProvider={this._layoutProvider}
rowRenderer={this._renderItem}
onEndReached={this._fetchOnEndReached}
renderFooter={this._renderFooter}
onVisibleIndicesChanged={this._onVisibleIndicesChanged}
onEndReachedThreshold={600}
renderAheadOffset={3000}
style={styles.recyclerListViewStyle}
scrollViewProps={{
refreshControl: (

),
}}
canChangeSize={true}
removeClippedSubviews={true}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
forceNonDeterministicRendering={true}
optimizeForInsertDeleteAnimations={true}
/>

@naqvitalha
Copy link
Collaborator

Hello folks, do me a favour and try version 3.1.0-alpha.7. The issue should be fixed and warning should only show up if there are legitimate duplicates. Watch out for the following:

  1. optimizeForInsertDeleteAnimations prop is a no op in this version. If you want to run layout animations you need to call a new API on RLV ref called prepareForLayoutAnimationRender, you can do this just before configuring layout animation for the next frame. Don't do this for large changes to data since RLV needs to stop recycling for animations to work well. After animations it resumes recycling.
  2. Aggressive use of layout animations in Android is sometimes causing errors. It seems like an issue with android implementation of these animations as I cannot reproduce the same issue on iOS. I don't see anything wrong with the RLV implementation. I'll continue to look into this.

I'd also appreciate if someone can try this with reanimated layout animations. Thanks!

@siddharth-kt
Copy link

siddharth-kt commented Mar 13, 2022

Hi @naqvitalha,
I tried 3.1.0-alpha7

But i am still facing below mentioned issue 🤔 (when deleting cells)

Warning: Encountered two children with the same key, 121|22. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

Any solution for this ?

@naqvitalha
Copy link
Collaborator

Can you try recreating a repro in expo and share? The error that you mentioned is not raised by recyclerlistview so it could be an issue with one of your components passing duplicate keys to react.

@siddharth-kt
Copy link

Thanks @naqvitalha, I think I got the point.

When ever i delete or add a element/cell in RecyclerListView dataProvider then new cell uses state values (example,
this.state={
liked: this.props.data.liked,
...
}
) of the element present on that position earlier.
Something similar to this issue : #432
I even tried disableRecycling={true} but it didn't worked.

To solve this issue I modified dataProvider like :
this.state = {
dataProvider: new DataProvider(
(r1, r2) => {
return r1.post.id !== r2.post.id;
},
index => {
const new_index =
this.state.dataProvider.getDataForIndex(index).post.id + '|' + index;
return new_index;
},
),
}
Following example from here : #432 (comment)
This actually caused this huge problem for me of stable Id collision.

Now i changed my every cell to re-render with new data.id updates, example.
{data.id is returned from my server and its unique)
...
componentDidUpdate(prevProps, prevState) {
if (this.props.data.id !== prevProps.data.id) {
this.setState({
liked: this.props.data.liked,
});
}
}
...

Am i doing it the correct way or is there any more optimized way of doing this ?
And actually my RecyclerListView code is connected to my local server so it won't work with expo, so i couldn't make expo snack out of it.

Thanks

@bohdan145
Copy link

Having the same issue.
Also, I have videos in my list,
If the video was at index 0 after I add a new data at index 0, I see the wrong data in a new cell, it shows video on the first item instead of new data I just added

@AlexSirenko
Copy link

AlexSirenko commented Jun 28, 2022

Have the same problem with 3.3.0-beta. On remove prepareForLayoutAnimationRender method was called, but still have no animation on IOS.
@naqvitalha

@naqvitalha
Copy link
Collaborator

@AlexSirenko Can you provide an expo sample? I can take a look.

@timothymiller
Copy link

timothymiller commented Aug 26, 2022

Edit: Here it is: 🖕

I wish there was a middle finger emoji for how much time I've wasted on this non-functioning library.

@Codelica
Copy link

For anyone in search of alternatives, FlashList (from Shopify) has been working well for me.

@TheJanitor337
Copy link

TheJanitor337 commented Dec 10, 2022

For anyone in search of alternatives, FlashList (from Shopify) has been working well for me.

Came here looking for the same warning happening with FlashList. 😆

Screenshot 2022-12-10 at 00 54 05

@angelxmoreno
Copy link

@TheJanitor337 same, did you ever find a solution?

@efstathiosntonas
Copy link

@TheJanitor337 did you ever managed to get around this? thanks

@TheJanitor337
Copy link

@angelxmoreno @efstathiosntonas Sorry for the delay! I didn't find a solution for this, but didn't debug very much as it was just a coding challenge project. I'll try to look at it this weekend to see if I can reproduce/fix what was happening. If I can, I'll share here. This FlashList GitHub issue might be of help: Shopify/flash-list#730

@efstathiosntonas
Copy link

efstathiosntonas commented Sep 29, 2023

@TheJanitor337 What I did and I've never encountered the stableId collision is to add the index to the keyExtractor like so:

 const keyExtractor = useCallback((item: any, i: number) => `${i}-${item.id}`, []);

I was constantly having issues with it since my list is 100% real-time but now the issues are gone. Can't explain why adding the index fixes it, it seems it works automagically now 🤷

@glenne
Copy link

glenne commented Dec 18, 2023

@TheJanitor337 What I did and I've never encountered the stableId collision is to add the index to the keyExtractor like so:

 const keyExtractor = useCallback((item: any, i: number) => `${i}-${item.id}`, []);

I was constantly having issues with it since my list is 100% real-time but now the issues are gone. Can't explain why adding the index fixes it, it seems it works automagically now 🤷

This works by essentially making the key different every time the list changes so recycling is effectively disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests