Skip to content

feat: LegendListDatasets#433

Draft
peterpme wants to merge 10 commits into
LegendApp:mainfrom
peterpme:feat/datasets
Draft

feat: LegendListDatasets#433
peterpme wants to merge 10 commits into
LegendApp:mainfrom
peterpme:feat/datasets

Conversation

@peterpme
Copy link
Copy Markdown
Contributor

Summary

  • Adds LegendListDatasets component for rendering multiple independent lists under a single shared ScrollView, header, and footer
  • Each dataset gets its own full LegendList engine (container pool, position map, size cache) wrapped in React.Activity for effect freezing
  • Optimizes tab switching: inactive layers use opacity: 0 + pointerEvents: none instead of height: 0 to keep native layout warm
  • Skips calculateItemsInView recalculation on reactivation when scroll position is still within the cached visible range
  • Adds ActivityOrFragment fallback for React 18 compatibility
  • Per-dataset overrides for keyExtractor, estimatedItemSize, getEstimatedItemSize, getFixedItemSize, and getItemType
  • Full LegendListRef imperative API delegates to the active dataset

New files

  • src/components/LegendListDatasets.tsx — parent component owning the shared ScrollView
  • src/components/DatasetLayer.tsx — per-dataset list engine (headless LegendList)
  • DATASETS.md — full documentation (architecture, usage, performance tips, footguns)
  • example/app/datasets/index.tsx — example app with 5 continent-based tabs

Test plan

  • Run example app, navigate to "Datasets (tabbed lists)"
  • Switch between continent tabs — verify instant switching with no flicker
  • Scroll down on one tab, switch to another, switch back — verify scroll position and items are preserved
  • Verify shared header (title + tab bar) scrolls with content
  • Verify inactive tabs don't receive scroll events or trigger recalculations
  • Test with recycleItems={true} and recycleItems={false}

jmeistrich and others added 8 commits December 23, 2025 12:53
Fix style prop of KeyboardAvoidingLegendList on Non-android platforms.
…tching

Introduces LegendListDatasets — a new component that keeps multiple data
arrays (datasets) simultaneously mounted in the React tree, using
React.Activity to defer work for inactive datasets. Switching between
datasets (e.g. spot/futures/favorites tabs) is near-instant: no React
reconciliation, no re-measurement, no re-allocation.

Architecture:
- One shared ScrollView and ListHeaderComponent (rendered once)
- Each dataset gets its own StateProvider + React.Activity wrapper
- Active dataset: Activity mode="visible", Containers in normal flow
- Inactive datasets: Activity mode="hidden", wrapper is position:absolute
  height:0 overflow:hidden — items live at translateY:-9999 but stay
  mounted so their sizes are cached
- On dataset switch: parent calls activate(scrollOffset) on the new
  active layer which runs calculateItemsInView with cached sizes

New exports: LegendListDatasets, DatasetEntry, LegendListDatasetsProps
- Wire up forwardedRef with full LegendListRef imperative API
  (scrollToIndex, scrollToItem, scrollToEnd, scrollToOffset,
   scrollIndexIntoView, scrollItemIntoView, getState,
   setScrollProcessingEnabled, setVisibleContentAnchorOffset,
   flashScrollIndicators, getNativeScrollRef, etc.)
  All methods delegate to the active dataset's ctx/state via
  getCtx()/getState() on DatasetLayerHandle

- Add initialScrollIndex / initialScrollOffset support
  Applied on first activation of each dataset; subsequent
  activations preserve the current scroll position

- Add snapToIndices support
  Each DatasetLayerInner calculates snap offsets via
  updateSnapToOffsets; LegendListDatasets subscribes to the
  active dataset's snapToOffsets via listen$ and passes them
  directly to the ScrollView

- Fix onScrollHandler to use activeIndexRef instead of captured
  activeIndex so it always routes to the correct layer without
  needing activeIndex in the dependency array
Three root causes fixed:

1. DatasetLayer had no React.memo — any parent re-render would cascade
   through all dataset layers unconditionally. Wrapped in typedMemo so
   sibling layers are skipped when only one dataset's data changes.

2. Callback props (renderItem, onEndReached, keyExtractor, etc.) were in
   sharedLayerProps deps, so un-memoized inline functions in the parent
   would invalidate sharedLayerProps every render, breaking the memo.
   Stabilized via refs + stable wrapper callbacks.

3. dataVersion was shared across all datasets — bumping it triggered
   calculateItemsInView({dataChanged:true}) on every layer simultaneously,
   clearing all positions for all datasets at once. Moved to DatasetEntry
   as a per-dataset field so only the affected dataset rebuilds.
@peterpme peterpme marked this pull request as draft April 16, 2026 15:15
Copy link
Copy Markdown

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

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: 33cb1e85dd

ℹ️ 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 on lines +553 to +554
{!showEmpty &&
datasets.map((dataset, index) => (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep dataset layers mounted when showing empty state

When showEmpty is true this branch skips rendering datasets.map(...), which unmounts all DatasetLayer instances, not just the active empty one. If a user switches to an empty tab and then back, every other tab loses its cached containers/positions and scroll state, defeating the tab-switch preservation this feature is meant to provide. Render the layers regardless and only overlay the empty component for the active dataset.

Useful? React with 👍 / 👎.

Comment on lines +197 to +198
if (prev !== activeIndex && activeIndex >= 0) {
layerRefs.current[activeIndex]?.activate(currentScrollRef.current);
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 Activate the initial dataset on first mount

The activation effect only calls activate(...) when the active index changes, but on first render prevActiveIndexRef already equals activeIndex, so the initial active layer is never activated. Because initialScrollIndex/initialScrollOffset are applied inside DatasetLayer.activate, initial scroll is ignored on mount and can later cause an unexpected jump the first time the user returns to that tab. The initial active dataset should be activated once after mount.

Useful? React with 👍 / 👎.

Comment on lines +217 to +220
const didDataChange = state.props.dataVersion !== dataVersion || state.props.data !== dataProp;
if (didDataChange) {
state.dataChangeNeedsScrollUpdate = true;
}
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 cached sizes when data changes without keyExtractor

On data changes this layer only sets dataChangeNeedsScrollUpdate, but unlike LegendList it never clears state.sizes/state.positions when no explicit keyExtractor is provided. In that configuration keys default to indices, so inserting/reordering items reuses stale size/position cache entries and can produce incorrect offsets/render windows until items are remeasured. DatasetLayer should mirror the reset behavior used in LegendList.tsx for this case.

Useful? React with 👍 / 👎.

- Use opacity: 0 + pointerEvents: none instead of height: 0 for inactive
  layers so native layout stays warm across tab switches
- Skip clearing scrollForNextCalculateItemsInView on activate so
  calculateItemsInView can early-return when scroll hasn't moved
- Add ActivityOrFragment fallback for React 18 compatibility
- Add datasets example app with continent-based tabs
- Add DATASETS.md documentation
dataVersion is only meaningful per-dataset (for detecting in-place mutations).
A shared top-level dataVersion has no clear target dataset, so remove it from
LegendListDatasetsProps. Also remove initialHeaderSize references that leaked
in from the feat/initial-header-size branch.
@peterpme peterpme changed the title feat: LegendListDatasets with optimized tab switching feat: LegendListDatasets Apr 29, 2026
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.

3 participants