Skip to content

New InfiniteLegendList proposal implementation - #495

Open
VladyslavMartynov10 wants to merge 11 commits into
LegendApp:mainfrom
VladyslavMartynov10:feat/infinite-legend-list
Open

New InfiniteLegendList proposal implementation#495
VladyslavMartynov10 wants to merge 11 commits into
LegendApp:mainfrom
VladyslavMartynov10:feat/infinite-legend-list

Conversation

@VladyslavMartynov10

Copy link
Copy Markdown
Contributor

Hey @jmeistrich, I need your feedback & ideas regarding API/implementation

Over a year ago, I initially implemented an animated InfiniteCarousel by using reanimated/Animated.ScrollView.

Everything works smoothly without any troubles, but creating a truly infinite list effect is not as easy as you can think from the first glance.

First of all, many libraries force you to create copy buffers from both sides [...left copy, middle items, right copy] in order to create infinite carousel effects, but it leads to some problems in the next use-cases:

When you have items like a player or any other items which hold individual state, with the copies effect you're fully losing state and can't manage it effectively + performance problems — with standard Animated.FlatList/Animated.ScrollView rendering 50+ elements is already a big challenge + if all of the items are also reanimated effect views it becomes a real performance gap.

The second trick is to use the translateY/translateX effect, which allows you to avoid the copy pattern, but it has its own drawback — it will be visually working in case you already have 6+ elements, for a 2 elements infinite carousel it works with some edge cases. And since nothing actually scrolls there, you also have to reimplement snap and momentum physics by hand with pan gestures, and it never feels 100% like a native ScrollView.

In my case, I've literally spent 1.5 months just to discover all of these use-cases and finally came to a solution. It is also a ton of hacks due to using the legend-state For component under the hood in order to create the lazy items init effect. By the time I was doing it, legend-list didn't support reanimated fully in terms of scroll, layout animations, etc... So with release v3 I believe I can fully delegate this to LegendList, so the API will be very flexible and nobody will spend so much time to implement it themselves.

What this PR adds:

  1. infiniteMode prop on LegendList. The data is repeated into a small odd number of virtual copies — but virtual means index math over an array of references, not mounted clones, so virtualization still keeps only ~3 mounted item components no matter the data size. Scroll starts at the middle copy, and when the offset drifts a few cycles from center, it is teleported back by whole cycles on momentum end. One cycle is the measured pixel size of one data copy (taken from real item positions), and since the content is periodic with exactly that period, the frame before and after the teleport is pixel-identical — the jump is invisible by construction, not by tuning.

  2. InfiniteLegendList component from @legendapp/list/infinite. A thin typed wrapper over the same engine: props that make no sense in a circular list (numColumns, onEndReached/onStartReached, header/footer, maintainScrollAtEnd, sticky headers) become compile-time errors instead of runtime warnings, infiniteIndex is a required renderItem prop, and copies is a first-class prop. It also takes a ListComponent prop, so it composes with AnimatedLegendList from @legendapp/list/reanimated (its extra props like sharedValues are typed through) — that's how you get UI-thread scroll-driven progress animations for the cards, dots, etc. without any state or reactions.

  3. Everything user-facing speaks real data indices. renderItem receives the real index plus infiniteIndex (position in the virtual space, for animations), viewability callbacks and keys are unwrapped back to the real ones, and scrollToIndex/scrollToItem on the ref take the shortest path around the circle — the same wrap logic as my original carousel.

  4. The state problem finally has a proper answer. Since every virtual copy renders the same item object, keeping per-item state in the data itself (immutably updated) or in an external store keyed by item id just works across recycling, wrap-around, and recentering. There is a dedicated fixture (infinite-carousel-state) that shows the correct pattern working live next to the two broken ones (useState bleeding across recycled containers, useRecyclingState resetting per copy).

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-07-09.at.23.23.42.mov

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a192f9d05d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/hooks/useInfiniteMode.ts Outdated
virtual[i] = data[i % dataLength];
}
return virtual;
}, [enabled, data, dataLength, copies]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild virtual data when dataVersion changes

When infiniteMode is enabled, callers can still use LegendList's documented dataVersion to publish in-place data-array replacements. Because virtualData is memoized only on the array reference, length, and copy count, a render that replaces data[i] and bumps dataVersion while keeping the same array identity reuses the old repeated array, so the inner list sees the new version but renders stale item objects and keys from the previous virtual data. Rebuild this memo when the data invalidation props change.

Useful? React with 👍 / 👎.

Comment thread src/hooks/useInfiniteMode.ts Outdated

const transformedProps = enabled
? {
...props,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear initialScrollAtEnd in infinite mode

If a caller uses the public infiniteMode prop on LegendList while initialScrollAtEnd is also set, this spread preserves initialScrollAtEnd even though the hook adds a middle-copy initialScrollIndex. LegendListInner gives initialScrollAtEnd precedence, so the list initializes at the last virtual item / edge instead of the middle copy promised by infinite mode, making the carousel start at an end clamp until a later recenter happens. Clear or ignore initialScrollAtEnd when infinite mode is active.

Useful? React with 👍 / 👎.

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

Successfully merging this pull request may close these issues.

1 participant