Skip to content

React v3.13.13 causes infinite re-renders after setting getItemKey() #1092

Closed

Description

@RaenonX

Describe the bug

As title. Only happens after going from v3.13.12 to v3.13.13.

Your minimal, reproducible example

See below.

Steps to reproduce

Run the following code, which is grabbed from the example with just an getItemKey() addition

import { useVirtualizer } from '@tanstack/react-virtual';

function App() {
  // The scrollable element for your list
  const parentRef = React.useRef(null)

  // The virtualizer
  const rowVirtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 35,
+    getItemKey: (index) => index,
  })

  return (
    <>
      {/* The scrollable element for your list */}
      <div
        ref={parentRef}
        style={{
          height: `400px`,
          overflow: 'auto', // Make it scroll!
        }}
      >
        {/* The large inner element to hold all of the items */}
        <div
          style={{
            height: `${rowVirtualizer.getTotalSize()}px`,
            width: '100%',
            position: 'relative',
          }}
        >
          {/* Only the visible items in the virtualizer, manually positioned to be in view */}
          {rowVirtualizer.getVirtualItems().map((virtualItem) => (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              Row {virtualItem.index}
            </div>
          ))}
        </div>
      </div>
    </>
  )
}

Expected behavior

No error.

How often does this bug happen?

Every time

Screenshots or Videos

Image

Platform

Windows 11 Edge (But I guess all browsers have this issue)

tanstack-virtual version

v3.13.13

TypeScript version

No response

Additional context

No response

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Activity

  1. j-marty commented on Dec 12, 2025

    @j-marty

    Move inline function for getItemKey to a callback function as a workaround. Like this:
    const getItemKey = useCallback((idx: number) => {...}, []);

  2. J4v4Scr1pt commented on Dec 12, 2025

    @J4v4Scr1pt

    Same here, above solution worked for me as well. 👍

  3. israel-lpz commented on Dec 13, 2025

    @israel-lpz

    Same here, above solution worked for me

  4. marcelpanse commented on Dec 14, 2025

    @marcelpanse

    Came here for the same bug. Putting it in a useCallback worked.

    Edit: we still had some problems in production with some users so we had to revert back to 3.13.12. This change breaks backwards compatibility.

  5. channyeintun commented on Dec 16, 2025

    @channyeintun

    Even with React Compiler, I still have to wrap it up inside useCallback?

  6. RaenonX commented on Dec 16, 2025

    @RaenonX
    Author

    Even with React Compiler, I still have to wrap it up inside useCallback?

    Using default strategy (syntax inferred or something), if you are not wrapping the function in a use* hook then yes

  7. zerkms commented on Dec 19, 2025

    @zerkms

    Now with this release it's now unclear what functions should be memoised, and which not.

    Eg, in the first post example:

        getScrollElement: () => parentRef.current,
        estimateSize: () => 35,
        getItemKey: (index) => index,

    it was mentioned that getItemKey must be memoised, but what about other two? Is it possible to know which must be memoised from documentation?

    I came to this report because I also have the same problem, but I do not use getItemKey and desperately try to find which of the options to blame 🤷

  8. RaenonX commented on Dec 19, 2025

    @RaenonX
    Author

    Now with this release it's now unclear what functions should be memoised, and which not.

    Eg, in the first post example:

        getScrollElement: () => parentRef.current,
        estimateSize: () => 35,
        getItemKey: (index) => index,

    it was mentioned that getItemKey must be memoised, but what about other two? Is it possible to know which must be memoised from documentation?

    I came to this report because I also have the same problem, but I do not use getItemKey and desperately try to find which of the options to blame 🤷

    This needs someone authentic enough to confirm about how it should work, but what I think should work is getItemKey() shouldn't also be memorized as well. It's more like a bug introduced in this version. For the other 2, the tutorial and my usages have them un-memozied and so far so good

  9. zerkms commented on Dec 19, 2025

    @zerkms

    Ok, I found what change happened to me: now (in 3.13.13) the onChange handler IS triggered when number of rows changes (no actual scrolling).

    And in 3.13.12 it DID NOT trigger 🤷

  10. mallio commented on Dec 22, 2025

    @mallio

    The documentation does mention that getItemKey needs to be memoized, but I had missed it and it wasn't causing trouble before.

    I'm fine if the solution is to memoize it, but in addition to the documentation mentioning it, I think there should be examples of it in use. I also wish it was more clear what needs to be memoized, because I pass a lot of unmemoized functions to createTable and they don't trigger rerenders..

  11. channyeintun commented on Dec 23, 2025

    @channyeintun

    It was working fine without using useCallback in the previous version. I just installed the latest version and got this issue. It is BREAKING CHANGES?

  12. zerkms commented on Dec 23, 2025

    @zerkms

    @channyeintun given it's been documented - it's not a BC but a bug fix.

  13. lott-ai commented on Jan 7, 2026

    @lott-ai

    The infinite re-rendering issue was introduced in #1085

    @piecyk updated the docs to include a note about memoizing getItemKey in #1103. I'd consider this a breaking change, since this memoizing requirement was not necessary before 🤷‍♂️

  14. piecyk commented on Jan 7, 2026

    @piecyk
    Collaborator

    @lott-ai Yeah, my bad, I agree this should be released as a major version. Even though the fix itself is reasonable, requiring getItemKey to be memoized wasn’t necessary before, and existing users can now run into infinite re-renders without any obvious indication of why.

    Personally, I’ve always memoized getItemKey since it’s part of the caching mechanism. There was already a real performance issue there, which is probably why I didn’t notice this earlier. Still, changing this behavior without a major release was a bad call, and it should be clearly communicated.

  15. zerkms commented on Jan 7, 2026

    @zerkms

    @lott-ai it was documented even before #1103: piecyk@106a0be

  16. RaenonX commented on Jan 7, 2026

    @RaenonX
    Author

    May I know why memoizing getItemKey() is strictly needed?

    And if it's strictly necessary, can we make it memoized in the implementation directly, saving us from not knowing that we need to memoize? Because apparently there are people not knowing this nuances, including myself. It was also unexpectedly working before.

  17. piecyk commented on Jan 7, 2026

    @piecyk
    Collaborator

    May I know why memoizing getItemKey() is strictly needed?

    @RaenonX the virtualizer automatically invalidates its measurement cache when measurement-affecting options change (for example, getItemKey). So if it isn’t memoized, every re-render recreates the list where the elements are placed.

    Overall the fix was unnecessary, getMeasurements() already invalidates on count changes via getMeasurementOptions() #1112

  18. RaenonX commented on Jan 7, 2026

    @RaenonX
    Author

    May I know why memoizing getItemKey() is strictly needed?

    @RaenonX the virtualizer automatically invalidates its measurement cache when measurement-affecting options change (for example, getItemKey). So if it isn’t memoized, every re-render recreates the list where the elements are placed.

    Overall the fix was unnecessary, getMeasurements() already invalidates on count changes via getMeasurementOptions() #1112

    I see, thanks for the clarification. Can I assume that after the next release (the PR you just created), memoization from the user is not needed anymore?

  19. piecyk commented on Jan 7, 2026

    @piecyk
    Collaborator

    I see, thanks for the clarification. Can I assume that after the next release (the PR you just created), memoization from the user is not needed anymore?

    Yep, it won’t be needed anymore, but it’s still highly recommended. Let’s save some RAM 🙂

    Overall, sorry everyone for the hassle.

  20. RaenonX commented on Jan 7, 2026

    @RaenonX
    Author

    No problem, and thanks for handling this! <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions