rn-legend-list

SKILL.md

Legend List — Best Practices & Performance Guide

@legendapp/list v2.0+ | React Native 0.72+ | Pure TypeScript — no native dependencies

Critical Rules

  1. Always provide keyExtractor with stable, unique IDs. Without it, Legend List cannot track items across data changes, causing unnecessary re-renders and layout bugs.
  2. Enable recycleItems={true} for any list beyond a handful of items. This is the single biggest performance win — it reuses view containers instead of creating/destroying them on scroll.
  3. Use useRecyclingState instead of useState inside recycled items. Regular useState carries state from the previous item when a container is reused, causing ghost data bugs.
  4. Use useRecyclingEffect to reset side effects (animations, subscriptions, timers) when an item container is recycled to a different data item.
  5. Provide accurate size estimates via estimatedItemSize or getEstimatedItemSize. Bad estimates cause layout jumping and scroll position inaccuracies.
  6. Use getFixedItemSize for any item whose height is known at render time. This skips measurement entirely — the fastest path.
  7. Use getItemType when recycleItems is enabled and your list has multiple visually different item types. This ensures containers are only recycled to items with the same structure.
  8. Memoize renderItem with useCallback and item components with React.memo. Inline arrow functions create new references every render, triggering unnecessary work.
  9. Enable maintainVisibleContentPosition (default in v2) for any list where items are added/removed above the viewport — especially chat and bidirectional lists.
  10. Never mutate the data array. Always produce a new array reference with immutable operations ([...prev, newItem], .map(), .filter()).

Code Review & Audit Action

When the user asks to review, check, audit, or optimize their Legend List code, follow this procedure:

  1. Scan all files importing from @legendapp/list (any subpath: /animated, /reanimated, /keyboard-controller).
  2. Check each file against the Critical Rules above and the Anti-Patterns table below. Collect: file path, line number, what's wrong, and the fix.
  3. Report findings in a table:
| File | Line | Issue | Severity | Fix |
|------|------|-------|----------|-----|
| src/Chat.tsx | 24 | useState inside recycled item | HIGH | Replace with useRecyclingState |
| src/Feed.tsx | 15 | Missing keyExtractor | HIGH | Add keyExtractor={(item) => item.id} |
| src/Feed.tsx | 15 | recycleItems not enabled | MEDIUM | Add recycleItems={true} |
  1. Severity levels:

    • CRITICAL: Will cause crashes or data corruption (duplicate keys, mutating data array)
    • HIGH: Causes visible jank or bugs (useState in recycled items, missing keyExtractor, no recycleItems on large lists, inline renderItem)
    • MEDIUM: Sub-optimal but functional (missing getItemType, inaccurate estimatedItemSize, no getFixedItemSize for known-size items)
    • LOW: Style/convention (could use better drawDistance tuning, missing React.memo on item component)
  2. Ask the user: "I found X issues. Want me to fix them all?" — then apply fixes if confirmed.

  3. After fixing, re-scan and report: total issues fixed, files modified, expected improvement.

Anti-Patterns Table

Anti-Pattern Why It's Bad Fix
useState in recycled item State carries over from previous item, showing wrong data useRecyclingState(() => initialValue)
Missing keyExtractor Can't track items — causes re-renders, layout bugs keyExtractor={(item) => item.id}
keyExtractor={() => Math.random()} Unstable keys defeat recycling and diffing Use stable unique ID from data
Inline renderItem arrow function New function reference every render, triggers re-render Wrap in useCallback
data[i].value = x; setData(data) Mutation — React won't detect changes setData(prev => prev.map(...))
recycleItems off on large list Creates/destroys views on scroll — slow recycleItems={true}
Missing getItemType with mixed item types Recycled containers get wrong structure, flash/re-layout getItemType={(item) => item.type}
estimatedItemSize wildly inaccurate Layout jumping, bad scroll position Measure real items, use getEstimatedItemSize for varying sizes
drawDistance too high with heavy items Renders too many expensive items offscreen Lower to 100-150 for heavy items
drawDistance too low with fast scroll Blank spaces appear during fast scroll Increase to 400-500

Performance Tuning Checklist

When optimizing a Legend List for maximum performance, apply these in order of impact:

  1. Enable recycling: recycleItems={true} + getItemType if mixed types
  2. Fix item sizes: getFixedItemSize for known heights, getEstimatedItemSize for varying
  3. Memoize everything: useCallback for renderItem, React.memo for item components
  4. Tune drawDistance: 250 (default) is good for most cases; lower for heavy items, higher for fast scrolling
  5. Use FastImage (or Expo Image) instead of RN <Image> for lists with many images — it caches properly and avoids re-downloads in recycled containers
  6. Offload expensive work: useMemo for computed data inside items, avoid layout thrashing

Installation

# Core
bun add @legendapp/list  # or npm/yarn

# Optional integrations
# Animated:   import from "@legendapp/list/animated"
# Reanimated: import from "@legendapp/list/reanimated"
# Keyboard:   import from "@legendapp/list/keyboard-controller"

Quick Start Template

import { LegendList, LegendListRef } from "@legendapp/list";

function MyList({ data }) {
  const listRef = useRef<LegendListRef>(null);

  const renderItem = useCallback(({ item }) => (
    <MemoizedItem item={item} />
  ), []);

  return (
    <LegendList
      ref={listRef}
      data={data}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      recycleItems={true}
      estimatedItemSize={80}
    />
  );
}

const MemoizedItem = React.memo(({ item }) => (
  <View style={styles.row}>
    <Text>{item.title}</Text>
  </View>
));

Chat UI Pattern (Most Requested)

Legend List excels at chat because it supports bottom-aligned content and bidirectional loading without the inverted hack that FlatList requires:

<LegendList
  data={messages}
  renderItem={renderMessage}
  keyExtractor={(msg) => msg.id}
  recycleItems={true}
  alignItemsAtEnd={true}              // Push content to bottom
  maintainScrollAtEnd={true}           // Auto-scroll on new messages
  maintainScrollAtEndThreshold={0.1}   // 10% from bottom
  onStartReached={loadOlderMessages}   // Load history at top
  onStartReachedThreshold={0.5}
  maintainVisibleContentPosition={true} // No jump when prepending
  estimatedItemSize={60}
/>

Key differences from FlatList chat:

  • No inverted needed — use alignItemsAtEnd instead
  • No scroll direction confusion — "start" is top, "end" is bottom
  • onStartReached loads older messages naturally

Reference Files

For deeper information, read these reference files as needed:

  • references/props-reference.md — Complete props API with types, defaults, and usage examples. Read when you need to configure a specific prop or understand its behavior.
  • references/use-cases.md — Full code examples for chat, infinite feed, grid, carousel, section list with sticky headers. Read when building a specific UI pattern.
  • references/troubleshooting.md — Detailed troubleshooting for 8 common issues (blank spaces, jumping items, recycled state, performance, onEndReached firing, image flashing, keyboard covering input, duplicate keys). Read when debugging a specific problem.
  • references/advanced.md — Animated integration (RN Animated + Reanimated), bidirectional infinite scroll, dynamic height measurement, custom scroll components, viewability tracking, heavy item optimization, hooks API (useRecyclingState, useRecyclingEffect, useViewability, useViewabilityAmount, useSyncLayout), ref methods (scrollToIndex, scrollToOffset, getState), and migration guides from FlatList/FlashList.
Weekly Installs
2
GitHub Stars
1
First Seen
11 days ago
Installed on
amp2
cline2
openclaw2
opencode2
cursor2
kimi-cli2