├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── biome.json
├── bun.lock
├── bunfig.toml
├── example
├── .gitignore
├── README.md
├── api
│ ├── data
│ │ ├── genres.json
│ │ ├── playlist
│ │ │ ├── 10402-10749.json
│ │ │ ├── 10402-10770.json
│ │ │ ├── 10402-37.json
│ │ │ ├── 10749-10752.json
│ │ │ ├── 10749-10770.json
│ │ │ ├── 10749-37.json
│ │ │ ├── 10749-878.json
│ │ │ ├── 10751-10402.json
│ │ │ ├── 10751-10752.json
│ │ │ ├── 10751-37.json
│ │ │ ├── 10751-53.json
│ │ │ ├── 10751-878.json
│ │ │ ├── 10751-9648.json
│ │ │ ├── 10752-37.json
│ │ │ ├── 12-10402.json
│ │ │ ├── 12-10749.json
│ │ │ ├── 12-18.json
│ │ │ ├── 12-27.json
│ │ │ ├── 12-35.json
│ │ │ ├── 14-36.json
│ │ │ ├── 14-878.json
│ │ │ ├── 16-10751.json
│ │ │ ├── 16-10770.json
│ │ │ ├── 16-35.json
│ │ │ ├── 16-36.json
│ │ │ ├── 16-53.json
│ │ │ ├── 18-10751.json
│ │ │ ├── 18-10752.json
│ │ │ ├── 18-37.json
│ │ │ ├── 18-53.json
│ │ │ ├── 18-878.json
│ │ │ ├── 27-10749.json
│ │ │ ├── 27-10770.json
│ │ │ ├── 28-10749.json
│ │ │ ├── 28-10751.json
│ │ │ ├── 28-10770.json
│ │ │ ├── 28-16.json
│ │ │ ├── 28-18.json
│ │ │ ├── 28-36.json
│ │ │ ├── 28-37.json
│ │ │ ├── 28-53.json
│ │ │ ├── 28-80.json
│ │ │ ├── 28-99.json
│ │ │ ├── 35-10749.json
│ │ │ ├── 35-10751.json
│ │ │ ├── 35-10752.json
│ │ │ ├── 35-27.json
│ │ │ ├── 35-36.json
│ │ │ ├── 35-53.json
│ │ │ ├── 35-80.json
│ │ │ ├── 36-37.json
│ │ │ ├── 36-878.json
│ │ │ ├── 36-9648.json
│ │ │ ├── 53-10752.json
│ │ │ ├── 80-10770.json
│ │ │ ├── 80-14.json
│ │ │ ├── 80-18.json
│ │ │ ├── 80-37.json
│ │ │ ├── 878-37.json
│ │ │ ├── 9648-10770.json
│ │ │ ├── 9648-37.json
│ │ │ ├── 9648-53.json
│ │ │ ├── 9648-878.json
│ │ │ ├── 99-10749.json
│ │ │ ├── 99-14.json
│ │ │ ├── 99-18.json
│ │ │ ├── 99-27.json
│ │ │ ├── 99-53.json
│ │ │ ├── 99-9648.json
│ │ │ └── index.ts
│ │ └── rows.json
│ └── index.ts
├── app.config.js
├── app.json
├── app
│ ├── (tabs)
│ │ ├── _layout.tsx
│ │ ├── cards.tsx
│ │ ├── index.tsx
│ │ ├── moviesL.tsx
│ │ └── moviesLR.tsx
│ ├── +not-found.tsx
│ ├── _layout.tsx
│ ├── accurate-scrollto-2
│ │ └── index.tsx
│ ├── accurate-scrollto
│ │ └── index.tsx
│ ├── add-to-end
│ │ └── index.tsx
│ ├── bidirectional-infinite-list
│ │ └── index.tsx
│ ├── cards-columns
│ │ └── index.tsx
│ ├── cards-flashlist
│ │ └── index.tsx
│ ├── cards-flatlist
│ │ └── index.tsx
│ ├── cards-no-recycle
│ │ └── index.tsx
│ ├── cards-renderItem.tsx
│ ├── chat-example
│ │ └── index.tsx
│ ├── chat-infinite
│ │ └── index.tsx
│ ├── chat-keyboard
│ │ └── index.tsx
│ ├── columns
│ │ └── index.tsx
│ ├── countries-flashlist
│ │ └── index.tsx
│ ├── countries-reorder
│ │ └── index.tsx
│ ├── countries
│ │ └── index.tsx
│ ├── extra-data
│ │ └── index.tsx
│ ├── filter-elements
│ │ ├── filter-data-provider.tsx
│ │ └── index.tsx
│ ├── initial-scroll-index-free-height
│ │ └── index.tsx
│ ├── initial-scroll-index-keyed
│ │ └── index.tsx
│ ├── initial-scroll-index
│ │ ├── index.tsx
│ │ └── renderFixedItem.tsx
│ ├── movies-flashlist
│ │ └── index.tsx
│ ├── mutable-cells
│ │ └── index.tsx
│ └── video-feed
│ │ └── index.tsx
├── assets
│ ├── fonts
│ │ └── SpaceMono-Regular.ttf
│ └── images
│ │ ├── adaptive-icon.png
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ ├── partial-react-logo.png
│ │ ├── react-logo.png
│ │ ├── react-logo@2x.png
│ │ ├── react-logo@3x.png
│ │ └── splash-icon.png
├── autoscroll.sh
├── bun.lock
├── bunfig.toml
├── components
│ ├── Breathe.tsx
│ ├── Circle.tsx
│ ├── Collapsible.tsx
│ ├── ExternalLink.tsx
│ ├── HapticTab.tsx
│ ├── HelloWave.tsx
│ ├── Movies.tsx
│ ├── ParallaxScrollView.tsx
│ ├── ThemedText.tsx
│ ├── ThemedView.tsx
│ ├── __tests__
│ │ ├── ThemedText-test.tsx
│ │ └── __snapshots__
│ │ │ └── ThemedText-test.tsx.snap
│ └── ui
│ │ ├── IconSymbol.ios.tsx
│ │ ├── IconSymbol.tsx
│ │ ├── TabBarBackground.ios.tsx
│ │ └── TabBarBackground.tsx
├── constants
│ ├── Colors.ts
│ ├── constants.ts
│ └── useScrollTest.ts
├── hooks
│ ├── useColorScheme.ts
│ ├── useColorScheme.web.ts
│ └── useThemeColor.ts
├── metro.config.js
├── package.json
├── scripts
│ └── reset-project.js
└── tsconfig.json
├── package.json
├── posttsup.ts
├── src
├── Container.tsx
├── Containers.tsx
├── ContextContainer.ts
├── DebugView.tsx
├── LeanView.tsx
├── LegendList.tsx
├── ListComponent.tsx
├── ListHeaderComponentContainer.tsx
├── ScrollAdjustHandler.ts
├── animated.tsx
├── constants.ts
├── helpers.ts
├── index.ts
├── keyboard-controller.tsx
├── reanimated.tsx
├── state.tsx
├── types.ts
├── useAnimatedValue.ts
├── useCombinedRef.ts
├── useInit.ts
├── useValue$.ts
└── viewability.ts
├── tsconfig.json
└── tsup.config.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [jmeistrich]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | example/android
4 | example/ios
5 | legendapp-list.tgz
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "printWidth": 120,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.organizeImports.biome": "explicit",
4 | "quickfix.biome": "explicit"
5 | },
6 | "editor.defaultFormatter": "biomejs.biome",
7 | "[typescriptreact]": {
8 | "editor.defaultFormatter": "biomejs.biome"
9 | },
10 | "[typescript]": {
11 | "editor.defaultFormatter": "biomejs.biome"
12 | },
13 | "editor.formatOnSave": true
14 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.0.15
2 | - Feat: Add a useIsLastItem hook
3 | - Feat: Support horizontal lists without an intrinsic height, it takes the maximum height of list items
4 | - Feat: Add onLoad prop
5 | - Fix: maintainVisibleContentPosition not working on horizontal lists
6 | - Perf: scrollForNextCalculateItemsInView was not taking drawDistance into account correctly
7 | - Perf: Improved the algorithm for allocating containers to items
8 | - Perf: Use useLayoutEffect in LegendList if available to get the outer ScrollView layout as soon as possible
9 |
10 | ## 1.0.14
11 | - Fix: A container changing size while inactive but not yet recycled could potentially overlap with elements onscreen if large enough
12 |
13 | ## 1.0.13
14 | - Fix: Missing React import in ListHeaderComponentContainer crashing some environments
15 | - Fix: `initialScrollIndex` was off by padding if using "padding" or "paddingVertical" props
16 |
17 | ## 1.0.12
18 | - Fix: Initial scroll index and scrollTo were not compensating for top padding
19 | - Fix: Removed an overly aggressive optimization that was sometimes causing blank spaces after scrolling
20 | - Fix: Adding a lot of items to the end with maintainScrollAtEnd could result in a large blank space
21 | - Fix: ListHeaderComponent sometimes not positioned correctly with maintainVisibleContentPosition
22 | - Fix: Gap styles not working with maintainVisibleContentPosition
23 |
24 | ## 1.0.11
25 | - Fix: scrollTo was sometimes showing gaps at the bottom or bottom after reaching the destination
26 |
27 | ## 1.0.10
28 | - Fix: Removed an optimization that only checked newly visible items, which could sometimes cause gaps in lists
29 | - Fix: Scroll history resets properly during scroll operations, which was causing gaps after scroll
30 | - Fix: Made scroll buffer calculations and scroll jump handling more reliable
31 |
32 | ## 1.0.9
33 | - Fix: Use the `use-sync-external-store` shim to support older versions of react
34 | - Fix: Lists sometimes leaving some gaps when reordering a list
35 | - Fix: Sometimes precomputing next scroll position for calculation incorrectly
36 |
37 | ## 1.0.8
38 | - Perf: The scroll buffering algorithm is smarter and adjusts based on scroll direction for better performance
39 | - Perf: The container-finding logic keeps index order, reducing gaps in rendering
40 | - Perf: Combine multiple hooks in Container to a single `useArray$` hook
41 |
42 | ## 1.0.7
43 | - Fix: Containers that move out of view are handled better
44 |
45 | ## 1.0.6
46 | - Fix: Average item size calculations are more accurate while scrolling
47 | - Fix: Items in view are handled better when data changes
48 | - Fix: Scroll position is maintained more accurately during updates
49 |
50 | ## 1.0.5
51 | - Fix: Fast scrolling sometimes caused elements to disappear
52 | - Fix: Out-of-range `scrollToIndex` calls are handled better
53 |
54 | ## 1.0.4
55 | - Fix: Container allocation is more efficient
56 | - Fix: Bidirectional infinite lists scroll better on the old architecture
57 | - Fix: Item size updates are handled more reliably
58 | - Fix: Container reuse logic is more accurate
59 | - Fix: Zero-size layouts are handled better in the old architecture
60 |
61 | ## 1.0.3
62 | - Fix: Items that are larger than the estimated size are handled correctly
63 |
64 | ## 1.0.2
65 | - Fix: Initial layout works better in the old architecture
66 | - Fix: Average size calculations are more accurate for bidirectional scrolling
67 | - Fix: Initial scroll index behavior is more precise
68 | - Fix: Item size calculations are more accurate overall
69 |
70 | ## 1.0.1
71 | - Fix: Total size calculations are correct when using average sizes
72 | - Fix: Keyboard avoiding behavior is improved for a smoother experience
73 |
74 | ## 1.0.0
75 | Initial release! Major changes if you're coming from a beta version:
76 |
77 | - Item hooks like `useRecyclingState` are no longer render props, but can be imported directly from `@legendapp/list`.
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Moo.do LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Legend List
2 |
3 | **Legend List** is a high-performance list component for **React Native**, written purely in Typescript with no native dependencies. It is a drop-in replacement for `FlatList` and `FlashList` with better performance, especially when handling dynamically sized items.
4 |
5 |
6 |
7 | ---
8 |
9 | ## 🤔 Why Legend List?
10 |
11 | * **Performance:** Designed from the ground up and heavily optimized for performance, it is faster than FlatList and other list libraries in most scenarios.
12 | * **Dynamic Item Sizes:** Natively supports items with varying heights without performance hits.
13 | * **Drop-in Replacement:** API compatibility with `FlatList` and `FlashList` for easier migration.
14 | * **100% JS:** No native module linking required, ensuring easy integration and compatibility across platforms.
15 | * **Lightweight:** Our goal is to keep LegendList as small of a dependency as possible. For more advanced use cases, we plan on supporting optional plugins. This ensures that we keep the package size as small as possible.
16 | * **Bidirectional infinite lists:** Supports infinite scrolling in both directions with no flashes or scroll jumping
17 | * **Chat UIs without inverted:** Chat UIs can align their content to the bottom and maintain scroll at end, so that the list doesn't need to be inverted, which causes weird behavior (in animations, etc...)
18 |
19 | For more information, listen to the Legend List episode of the [React Native Radio Podcast](https://infinite.red/react-native-radio/rnr-325-legend-list-with-jay-meistrich) and the [livestream with Expo](https://www.youtube.com/watch?v=XpZMveUCke8).
20 |
21 | ---
22 | ## ✨ Additional Features
23 |
24 | Beyond standard `FlatList` capabilities:
25 |
26 | * `recycleItems`: (boolean) Toggles item component recycling.
27 | * `true`: Reuses item components for optimal performance. Be cautious if your item components contain local state, as it might be reused unexpectedly.
28 | * `false` (default): Creates new item components every time. Less performant but safer if items have complex internal state.
29 | * `maintainScrollAtEnd`: (boolean) If `true` and the user is scrolled near the bottom (within `maintainScrollAtEndThreshold * screen height`), the list automatically scrolls to the end when items are added or heights change. Useful for chat interfaces.
30 | * `alignItemsAtEnd`: (boolean) Useful for chat UIs, content smaller than the View will be aligned to the bottom of the list.
31 |
32 | ---
33 |
34 | ## 📚 Documentation
35 |
36 | For comprehensive documentation, guides, and the full API reference, please visit:
37 |
38 | ➡️ **[Legend List Documentation Site](https://www.legendapp.com/open-source/list)**
39 |
40 | ---
41 |
42 | ## 💻 Usage
43 |
44 | ### Installation
45 |
46 | ```bash
47 | # Using Bun
48 | bun add @legendapp/list
49 |
50 | # Using npm
51 | npm install @legendapp/list
52 |
53 | # Using Yarn
54 | yarn add @legendapp/list
55 | ```
56 |
57 | ### Example
58 | ```tsx
59 | import React, { useRef } from "react"
60 | import { View, Image, Text, StyleSheet } from "react-native"
61 | import { LegendList, LegendListRef, LegendListRenderItemProps } from "@legendapp/list"
62 |
63 | // Define the type for your data items
64 | interface UserData {
65 | id: string;
66 | name: string;
67 | photoUri: string;
68 | }
69 |
70 | const LegendListExample = () => {
71 | // Optional: Ref for accessing list methods (e.g., scrollTo)
72 | const listRef = useRef(null)
73 |
74 | const data = []
75 |
76 | const renderItem = ({ item }: LegendListRenderItemProps) => {
77 | return (
78 |
79 |
80 | {item.name}
81 |
82 | )
83 | }
84 |
85 | return (
86 | item.id}
93 | recycleItems={true}
94 |
95 | // Recommended if data can change
96 | maintainVisibleContentPosition
97 |
98 | ref={listRef}
99 | />
100 | )
101 | }
102 |
103 | export default LegendListExample
104 |
105 | ```
106 |
107 | ---
108 |
109 | ## How to Build
110 |
111 | 1. `bun i`
112 | 2. `bun run build` will build the package to the `dist` folder.
113 |
114 | ## Running the Example
115 |
116 | 1. `cd example`
117 | 2. `bun i`
118 | 3. `bun run ios`
119 |
120 | ## PRs gladly accepted!
121 |
122 | There's not a ton of code so hopefully it's easy to contribute. If you want to add a missing feature or fix a bug please post an issue to see if development is already in progress so we can make sure to not duplicate work 😀.
123 |
124 | ## Upcoming Roadmap
125 |
126 | - [] Column spans
127 | - [] overrideItemLayout
128 | - [] Sticky headers
129 | - [] Masonry layout
130 | - [] getItemType
131 | - [] React DOM implementation
132 |
133 | ## Community
134 |
135 | Join us on [Discord](https://discord.gg/tuW2pAffjA) to get involved with the Legend community.
136 |
137 | ## 👩⚖️ License
138 |
139 | [MIT](LICENSE)
140 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "ignore": []
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "formatWithErrors": false,
15 | "indentStyle": "space",
16 | "indentWidth": 4,
17 | "lineEnding": "lf",
18 | "lineWidth": 120
19 | },
20 | "organizeImports": {
21 | "enabled": true
22 | },
23 | "linter": {
24 | "enabled": true,
25 | "rules": {
26 | "recommended": true,
27 | "style": {
28 | "noNonNullAssertion": "off"
29 | },
30 | "suspicious": {
31 | "noArrayIndexKey": "off",
32 | "noExplicitAny": "off",
33 | "noConfusingVoidType": "off"
34 | },
35 | "correctness": {
36 | "noUnusedImports": "warn",
37 | "useExhaustiveDependencies": "off"
38 | }
39 | }
40 | },
41 | "javascript": {
42 | "formatter": {
43 | "quoteStyle": "double"
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/bunfig.toml:
--------------------------------------------------------------------------------
1 | [install]
2 | saveTextLockfile = true
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
17 | # The following patterns were generated by expo-cli
18 |
19 | expo-env.d.ts
20 | # @end expo-cli
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to LegendList test application 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 | This app is main development playground for testing LegendList features
5 |
6 | ## Get started
7 |
8 | 1. Install dependencies
9 |
10 | ```bash
11 | npm install
12 | ```
13 |
14 | 2. Start the app
15 |
16 | ```bash
17 | npx expo start
18 | ```
19 |
20 | In the output, you'll find options to open the app in a
21 |
22 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
23 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
24 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
25 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
26 |
27 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
28 |
29 | ## Testing on old arhitecture
30 |
31 | LegendList is by default ran on the react native new architecture. It's important to check compatibility with old architecture.
32 |
33 | To build version for old architecture:
34 | 1. delete your ios and anrdoid folders
35 | 2. build new versions
36 | ```
37 | OLD_ARCH=TRUE bun android
38 | OLD_ARCH=TRUE bun ios
39 | ```
40 | Those applications will have separate app name list-test-oldarch and different app id, so both old and new architectures can be tested on same device.
--------------------------------------------------------------------------------
/example/api/data/genres.json:
--------------------------------------------------------------------------------
1 | {
2 | "genres": [
3 | {"id": 28, "name": "Action"},
4 | {"id": 12, "name": "Adventure"},
5 | {"id": 16, "name": "Animation"},
6 | {"id": 35, "name": "Comedy"},
7 | {"id": 80, "name": "Crime"},
8 | {"id": 99, "name": "Documentary"},
9 | {"id": 18, "name": "Drama"},
10 | {"id": 10751, "name": "Family"},
11 | {"id": 14, "name": "Fantasy"},
12 | {"id": 36, "name": "History"},
13 | {"id": 27, "name": "Horror"},
14 | {"id": 10402, "name": "Music"},
15 | {"id": 9648, "name": "Mystery"},
16 | {"id": 10749, "name": "Romance"},
17 | {"id": 878, "name": "Science Fiction"},
18 | {"id": 10770, "name": "TV Movie"},
19 | {"id": 53, "name": "Thriller"},
20 | {"id": 10752, "name": "War"},
21 | {"id": 37, "name": "Western"}
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/example/api/data/playlist/index.ts:
--------------------------------------------------------------------------------
1 | import {Movie} from '../..';
2 |
3 | export const playlists: {[key: string]: () => Movie[]} = {
4 | '10751-9648': () => require('./10751-9648.json'),
5 | '28-53': () => require('./28-53.json'),
6 | '14-36': () => require('./14-36.json'),
7 | '99-18': () => require('./99-18.json'),
8 | '12-18': () => require('./12-18.json'),
9 | '28-16': () => require('./28-16.json'),
10 | '9648-37': () => require('./9648-37.json'),
11 | '28-10749': () => require('./28-10749.json'),
12 | '99-10749': () => require('./99-10749.json'),
13 | '99-9648': () => require('./99-9648.json'),
14 | '10751-10402': () => require('./10751-10402.json'),
15 | '35-10752': () => require('./35-10752.json'),
16 | '10751-37': () => require('./10751-37.json'),
17 | '9648-878': () => require('./9648-878.json'),
18 | '9648-53': () => require('./9648-53.json'),
19 | '10749-10752': () => require('./10749-10752.json'),
20 | '99-27': () => require('./99-27.json'),
21 | '27-10770': () => require('./27-10770.json'),
22 | '18-10752': () => require('./18-10752.json'),
23 | '35-10749': () => require('./35-10749.json'),
24 | '36-9648': () => require('./36-9648.json'),
25 | '10752-37': () => require('./10752-37.json'),
26 | '16-53': () => require('./16-53.json'),
27 | '99-53': () => require('./99-53.json'),
28 | '35-10751': () => require('./35-10751.json'),
29 | '35-80': () => require('./35-80.json'),
30 | '80-14': () => require('./80-14.json'),
31 | '10751-878': () => require('./10751-878.json'),
32 | '10402-10749': () => require('./10402-10749.json'),
33 | '16-35': () => require('./16-35.json'),
34 | '80-10770': () => require('./80-10770.json'),
35 | '16-36': () => require('./16-36.json'),
36 | '28-10770': () => require('./28-10770.json'),
37 | '18-10751': () => require('./18-10751.json'),
38 | '99-14': () => require('./99-14.json'),
39 | '80-37': () => require('./80-37.json'),
40 | '10402-10770': () => require('./10402-10770.json'),
41 | '10751-53': () => require('./10751-53.json'),
42 | '35-53': () => require('./35-53.json'),
43 | '16-10751': () => require('./16-10751.json'),
44 | '35-27': () => require('./35-27.json'),
45 | '28-37': () => require('./28-37.json'),
46 | '28-80': () => require('./28-80.json'),
47 | '36-37': () => require('./36-37.json'),
48 | '12-35': () => require('./12-35.json'),
49 | '53-10752': () => require('./53-10752.json'),
50 | '35-36': () => require('./35-36.json'),
51 | '12-10749': () => require('./12-10749.json'),
52 | '16-10770': () => require('./16-10770.json'),
53 | '28-18': () => require('./28-18.json'),
54 | '10749-10770': () => require('./10749-10770.json'),
55 | '27-10749': () => require('./27-10749.json'),
56 | '10749-878': () => require('./10749-878.json'),
57 | '10402-37': () => require('./10402-37.json'),
58 | '18-37': () => require('./18-37.json'),
59 | '28-99': () => require('./28-99.json'),
60 | '14-878': () => require('./14-878.json'),
61 | '18-878': () => require('./18-878.json'),
62 | '28-10751': () => require('./28-10751.json'),
63 | '80-18': () => require('./80-18.json'),
64 | '878-37': () => require('./878-37.json'),
65 | '10749-37': () => require('./10749-37.json'),
66 | '10751-10752': () => require('./10751-10752.json'),
67 | '12-27': () => require('./12-27.json'),
68 | '9648-10770': () => require('./9648-10770.json'),
69 | '12-10402': () => require('./12-10402.json'),
70 | '36-878': () => require('./36-878.json'),
71 | '18-53': () => require('./18-53.json'),
72 | '28-36': () => require('./28-36.json'),
73 | };
74 |
--------------------------------------------------------------------------------
/example/api/data/rows.json:
--------------------------------------------------------------------------------
1 | [{"id":"10751-9648","title":"Family & Mystery"},{"id":"28-53","title":"Action & Thriller"},{"id":"14-36","title":"Fantasy & History"},{"id":"99-18","title":"Documentary & Drama"},{"id":"12-18","title":"Adventure & Drama"},{"id":"28-16","title":"Action & Animation"},{"id":"9648-37","title":"Mystery & Western"},{"id":"28-10749","title":"Action & Romance"},{"id":"99-10749","title":"Documentary & Romance"},{"id":"99-9648","title":"Documentary & Mystery"},{"id":"10751-10402","title":"Family & Music"},{"id":"35-10752","title":"Comedy & War"},{"id":"10751-37","title":"Family & Western"},{"id":"9648-878","title":"Mystery & Science Fiction"},{"id":"9648-53","title":"Mystery & Thriller"},{"id":"10749-10752","title":"Romance & War"},{"id":"99-27","title":"Documentary & Horror"},{"id":"27-10770","title":"Horror & TV Movie"},{"id":"18-10752","title":"Drama & War"},{"id":"35-10749","title":"Comedy & Romance"},{"id":"36-9648","title":"History & Mystery"},{"id":"10752-37","title":"War & Western"},{"id":"16-53","title":"Animation & Thriller"},{"id":"99-53","title":"Documentary & Thriller"},{"id":"35-10751","title":"Comedy & Family"},{"id":"35-80","title":"Comedy & Crime"},{"id":"80-14","title":"Crime & Fantasy"},{"id":"10751-878","title":"Family & Science Fiction"},{"id":"10402-10749","title":"Music & Romance"},{"id":"16-35","title":"Animation & Comedy"},{"id":"80-10770","title":"Crime & TV Movie"},{"id":"16-36","title":"Animation & History"},{"id":"28-10770","title":"Action & TV Movie"},{"id":"18-10751","title":"Drama & Family"},{"id":"99-14","title":"Documentary & Fantasy"},{"id":"80-37","title":"Crime & Western"},{"id":"10402-10770","title":"Music & TV Movie"},{"id":"10751-53","title":"Family & Thriller"},{"id":"35-53","title":"Comedy & Thriller"},{"id":"16-10751","title":"Animation & Family"},{"id":"35-27","title":"Comedy & Horror"},{"id":"28-37","title":"Action & Western"},{"id":"28-80","title":"Action & Crime"},{"id":"36-37","title":"History & Western"},{"id":"12-35","title":"Adventure & Comedy"},{"id":"53-10752","title":"Thriller & War"},{"id":"35-36","title":"Comedy & History"},{"id":"12-10749","title":"Adventure & Romance"},{"id":"16-10770","title":"Animation & TV Movie"},{"id":"28-18","title":"Action & Drama"},{"id":"10749-10770","title":"Romance & TV Movie"},{"id":"27-10749","title":"Horror & Romance"},{"id":"10749-878","title":"Romance & Science Fiction"},{"id":"10402-37","title":"Music & Western"},{"id":"18-37","title":"Drama & Western"},{"id":"28-99","title":"Action & Documentary"},{"id":"14-878","title":"Fantasy & Science Fiction"},{"id":"18-878","title":"Drama & Science Fiction"},{"id":"28-10751","title":"Action & Family"},{"id":"80-18","title":"Crime & Drama"},{"id":"878-37","title":"Science Fiction & Western"},{"id":"10749-37","title":"Romance & Western"},{"id":"10751-10752","title":"Family & War"},{"id":"12-27","title":"Adventure & Horror"},{"id":"9648-10770","title":"Mystery & TV Movie"},{"id":"12-10402","title":"Adventure & Music"},{"id":"36-878","title":"History & Science Fiction"},{"id":"18-53","title":"Drama & Thriller"},{"id":"28-36","title":"Action & History"}]
--------------------------------------------------------------------------------
/example/api/index.ts:
--------------------------------------------------------------------------------
1 | import {PixelRatio} from 'react-native';
2 |
3 | export interface Playlist {
4 | id: string;
5 | title: string;
6 | }
7 |
8 | export interface Movie {
9 | adult: boolean;
10 | backdrop_path: string;
11 | genre_ids: number[];
12 | id: number;
13 | original_language: string;
14 | original_title: string;
15 | overview: string;
16 | popularity: number;
17 | poster_path: string;
18 | release_date: string;
19 | title: string;
20 | video: boolean;
21 | vote_average: number;
22 | vote_count: number;
23 | }
24 |
25 | export const IMAGE_SIZE = {
26 | width: 92,
27 | height: 138,
28 | };
29 |
30 | const POSTER_SIZES = [92, 154, 185, 342, 500, 780];
31 | const POSTER_SIZE = POSTER_SIZES.find(
32 | size => size >= PixelRatio.getPixelSizeForLayoutSize(IMAGE_SIZE.width),
33 | );
34 |
35 | export const getImageUrl = (path: string) =>
36 | `https://image.tmdb.org/t/p/w${POSTER_SIZE}${path}`;
37 |
--------------------------------------------------------------------------------
/example/app.config.js:
--------------------------------------------------------------------------------
1 | const OLD_ARCH = process.env.OLD_ARCH === 'TRUE';
2 | const RELEASE = process.env.RELEASE === 'TRUE';
3 |
4 | export default ({ config }) => {
5 | const bundleIdentifier = `com.legendapp.listtest${OLD_ARCH ? '.o' : ''}${OLD_ARCH ? '.r' : ''}`;
6 | return {
7 | ...config,
8 | newArchEnabled: !OLD_ARCH,
9 | ios: {
10 | supportsTablet: true,
11 | bundleIdentifier,
12 | },
13 | android: {
14 | adaptiveIcon: {
15 | foregroundImage: './assets/images/adaptive-icon.png',
16 | backgroundColor: '#ffffff',
17 | },
18 | package: bundleIdentifier,
19 | },
20 | name: `list-test${OLD_ARCH ? '-o' : ''}${RELEASE ? '-r' : ''}`,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "list-test",
4 | "slug": "list-test",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "ios": {
12 | "supportsTablet": true,
13 | "bundleIdentifier": "com.legendapp.listtest"
14 | },
15 | "android": {
16 | "adaptiveIcon": {
17 | "foregroundImage": "./assets/images/adaptive-icon.png",
18 | "backgroundColor": "#ffffff"
19 | },
20 | "package": "com.legendapp.listtest"
21 | },
22 | "web": {
23 | "bundler": "metro",
24 | "output": "static",
25 | "favicon": "./assets/images/favicon.png"
26 | },
27 | "plugins": [
28 | "expo-router",
29 | [
30 | "expo-splash-screen",
31 | {
32 | "image": "./assets/images/splash-icon.png",
33 | "imageWidth": 200,
34 | "resizeMode": "contain",
35 | "backgroundColor": "#ffffff"
36 | }
37 | ],
38 | "expo-font"
39 | ],
40 | "experiments": {
41 | "typedRoutes": true,
42 | "reactCompiler": false
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/example/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from "expo-router";
2 | import { Platform } from "react-native";
3 |
4 | import { HapticTab } from "@/components/HapticTab";
5 | import { IconSymbol } from "@/components/ui/IconSymbol";
6 | import TabBarBackground from "@/components/ui/TabBarBackground";
7 | import { Colors } from "@/constants/Colors";
8 | import { useColorScheme } from "@/hooks/useColorScheme";
9 |
10 | export default function TabLayout() {
11 | const colorScheme = useColorScheme();
12 |
13 | return (
14 |
28 | ,
33 | }}
34 | />
35 | (
40 |
41 | ),
42 | }}
43 | />
44 | ,
49 | }}
50 | />
51 | ,
56 | }}
57 | />
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/example/app/(tabs)/cards.tsx:
--------------------------------------------------------------------------------
1 | import { type Item, renderItem } from "@/app/cards-renderItem";
2 | import { DO_SCROLL_TEST, DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3 | import { useScrollTest } from "@/constants/useScrollTest";
4 | import { LegendList, type LegendListRef } from "@legendapp/list";
5 | import { useRef, useState } from "react";
6 | import { LogBox, Platform, StyleSheet, View } from "react-native";
7 |
8 | LogBox.ignoreLogs(["Open debugger"]);
9 |
10 | interface CardsProps {
11 | numColumns?: number;
12 | }
13 |
14 | export default function Cards({ numColumns = 1 }: CardsProps) {
15 | const listRef = useRef(null);
16 |
17 | const [data, setData] = useState- (
18 | () =>
19 | Array.from({ length: 1000 }, (_, i) => ({
20 | id: i.toString(),
21 | })) as any[],
22 | );
23 |
24 | if (DO_SCROLL_TEST) {
25 | useScrollTest((offset) => {
26 | listRef.current?.scrollToOffset({
27 | offset: offset,
28 | animated: true,
29 | });
30 | });
31 | }
32 |
33 | // Note that if benchmarking against other cards implementations
34 | // it should use the same props
35 | return (
36 |
37 | item.id}
42 | estimatedItemSize={ESTIMATED_ITEM_LENGTH}
43 | drawDistance={DRAW_DISTANCE}
44 | recycleItems={true}
45 | ListHeaderComponent={}
46 | ListHeaderComponentStyle={styles.listHeader}
47 | extraData={{ recycleState: true }}
48 | />
49 |
50 | );
51 | }
52 |
53 | const styles = StyleSheet.create({
54 | listHeader: {
55 | alignSelf: "center",
56 | height: 100,
57 | width: 100,
58 | backgroundColor: "#456AAA",
59 | borderRadius: 12,
60 | marginHorizontal: 8,
61 | marginVertical: 8,
62 | },
63 | listEmpty: {
64 | flex: 1,
65 | justifyContent: "center",
66 | alignItems: "center",
67 | backgroundColor: "#6789AB",
68 | paddingVertical: 16,
69 | },
70 | outerContainer: {
71 | backgroundColor: "#456",
72 | bottom: Platform.OS === "ios" ? 82 : 0,
73 | },
74 | scrollContainer: {},
75 | listContainer: {
76 | width: 400,
77 | maxWidth: "100%",
78 | marginHorizontal: "auto",
79 | },
80 | });
81 |
--------------------------------------------------------------------------------
/example/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText } from "@/components/ThemedText";
2 | import { ThemedView } from "@/components/ThemedView";
3 | import { LegendList } from "@legendapp/list";
4 | import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
5 | import { Link, type LinkProps } from "expo-router";
6 | import { useCallback } from "react";
7 | import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, View, useColorScheme } from "react-native";
8 | import { SafeAreaView } from "react-native-safe-area-context";
9 |
10 | // @ts-expect-error nativeFabricUIManager is not defined in the global object types
11 | export const IsNewArchitecture = global.nativeFabricUIManager != null;
12 |
13 | type ListElement = {
14 | id: number;
15 | title: string;
16 | url: LinkProps["href"];
17 | index: number;
18 | };
19 |
20 | const data: ListElement[] = [
21 | {
22 | title: "Bidirectional Infinite List",
23 | url: "/bidirectional-infinite-list",
24 | },
25 | {
26 | title: "Chat example",
27 | url: "/chat-example",
28 | },
29 | {
30 | title: "Infinite chat",
31 | url: "/chat-infinite",
32 | },
33 | {
34 | title: "Countries List",
35 | url: "/countries",
36 | },
37 | {
38 | title: "Accurate scrollToIndex",
39 | url: "/accurate-scrollto",
40 | },
41 | {
42 | title: "Accurate scrollToIndex 2",
43 | url: "/accurate-scrollto-2",
44 | },
45 | {
46 | title: "Columns",
47 | url: "/columns",
48 | },
49 |
50 | {
51 | title: "Cards Columns",
52 | url: "/cards-columns",
53 | },
54 | {
55 | title: "Chat keyboard",
56 | url: "/chat-keyboard",
57 | },
58 | {
59 | title: "Movies FlashList",
60 | url: "/movies-flashlist",
61 | },
62 | {
63 | title: "Initial scroll index precise navigation",
64 | url: "/initial-scroll-index",
65 | },
66 | {
67 | title: "Initial scroll index(free element height)",
68 | url: "/initial-scroll-index-free-height",
69 | },
70 | {
71 | title: "Initial Scroll Index keyed",
72 | url: "/initial-scroll-index-keyed",
73 | },
74 | {
75 | title: "Mutable elements",
76 | url: "/mutable-cells",
77 | },
78 | {
79 | title: "Extra data",
80 | url: "/extra-data",
81 | },
82 | {
83 | title: "Countries List(FlashList)",
84 | url: "/countries-flashlist",
85 | },
86 | {
87 | title: "Filter elements",
88 | url: "/filter-elements",
89 | },
90 | {
91 | title: "Video feed",
92 | url: "/video-feed",
93 | },
94 | {
95 | title: "Countries Reorder",
96 | url: "/countries-reorder",
97 | },
98 | {
99 | title: "Cards FlashList",
100 | url: "/cards-flashlist",
101 | },
102 | {
103 | title: "Cards no recycle",
104 | url: "/cards-no-recycle",
105 | },
106 | {
107 | title: "Cards FlatList",
108 | url: "/cards-flatlist",
109 | },
110 | {
111 | title: "Add to the end",
112 | url: "/add-to-end",
113 | },
114 | ].map(
115 | (v, i) =>
116 | ({
117 | ...v,
118 | id: i + 1,
119 | }) as ListElement,
120 | );
121 |
122 | const RightIcon = () => ›;
123 |
124 | const ListItem = ({ title, url, index }: ListElement) => {
125 | const theme = useColorScheme() ?? "light";
126 |
127 | return (
128 |
129 |
130 |
137 | {title}
138 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | const ListElements = () => {
146 | const height = useBottomTabBarHeight();
147 | const onLayout = useCallback((event: LayoutChangeEvent) => {
148 | console.log("onlayout", event.nativeEvent.layout);
149 | }, []);
150 | return (
151 |
152 | }
156 | keyExtractor={(item) => item.id.toString()}
157 | onItemSizeChanged={(info) => {
158 | console.log("item size changed", info);
159 | }}
160 | ListHeaderComponent={
161 |
162 |
163 | {IsNewArchitecture ? "New" : "Old"} Architecture, {__DEV__ ? "DEV" : "PROD"}
164 |
165 |
166 | }
167 | ListFooterComponent={}
168 | ListFooterComponentStyle={{ height: Platform.OS === "ios" ? height : 0 }}
169 | onLayout={onLayout}
170 | />
171 |
172 | );
173 | };
174 |
175 | const styles = StyleSheet.create({
176 | container: {
177 | flex: 1,
178 | },
179 | item: {
180 | padding: 16,
181 | height: 60,
182 | borderBottomWidth: 1,
183 | width: "100%",
184 | flexDirection: "row",
185 | justifyContent: "space-between",
186 | },
187 | });
188 |
189 | export default ListElements;
190 |
--------------------------------------------------------------------------------
/example/app/(tabs)/moviesL.tsx:
--------------------------------------------------------------------------------
1 | import Movies from "@/components/Movies";
2 |
3 | const App = () => {
4 | return ;
5 | };
6 |
7 | export default App;
8 |
--------------------------------------------------------------------------------
/example/app/(tabs)/moviesLR.tsx:
--------------------------------------------------------------------------------
1 | import Movies from "@/components/Movies";
2 |
3 | const App = () => {
4 | return ;
5 | };
6 |
7 | export default App;
8 |
--------------------------------------------------------------------------------
/example/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from "expo-router";
2 | import { StyleSheet } from "react-native";
3 |
4 | import { ThemedText } from "@/components/ThemedText";
5 | import { ThemedView } from "@/components/ThemedView";
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: "center",
25 | justifyContent: "center",
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/example/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
2 | import { Stack } from "expo-router";
3 | import { StatusBar } from "expo-status-bar";
4 | import "react-native-reanimated";
5 | import { useColorScheme } from "@/hooks/useColorScheme";
6 | import { GestureHandlerRootView } from "react-native-gesture-handler";
7 | import { ReanimatedLogLevel } from "react-native-reanimated";
8 | import { configureReanimatedLogger } from "react-native-reanimated";
9 | import { enableFreeze } from "react-native-screens";
10 |
11 | // Prevent the splash screen from auto-hiding before asset loading is complete.
12 | enableFreeze(); // freeze inactive tabs in the tabbar, to improve benchmarking accuracy
13 |
14 | configureReanimatedLogger({
15 | level: ReanimatedLogLevel.warn,
16 | strict: false, // Reanimated runs in strict mode by default
17 | });
18 |
19 | export default function RootLayout() {
20 | const colorScheme = useColorScheme();
21 | console.log("starting in", __DEV__ ? "dev" : "prod");
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/example/app/accurate-scrollto-2/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList, type LegendListRef, type LegendListRenderItemProps } from "@legendapp/list";
2 | import { useEffect, useRef } from "react";
3 | import { Text, View } from "react-native";
4 | const App = () => {
5 | const dummyData = Array.from({ length: 100 }, (_, index) => ({
6 | id: index,
7 | name: `Item ${index}`,
8 | value: Math.floor(Math.random() * 1000),
9 | isActive: Math.random() > 0.5,
10 | height: Math.floor(Math.random() * 200) + 50, // Random height between 50 and 250
11 | createdAt: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString(),
12 | }));
13 | const renderItem = (props: LegendListRenderItemProps) => {
14 | return (
15 |
25 | {props.item.name}
26 |
27 | );
28 | };
29 | const listRef = useRef(null);
30 | useEffect(() => {
31 | setTimeout(() => {
32 | listRef.current?.scrollToIndex({
33 | index: 80,
34 | animated: true,
35 | });
36 | }, 1000);
37 | }, []);
38 | return (
39 |
40 | `id${item.id}`}
45 | renderItem={renderItem}
46 | estimatedItemSize={25}
47 | recycleItems
48 | />
49 |
50 | );
51 | };
52 | export default App;
53 |
--------------------------------------------------------------------------------
/example/app/accurate-scrollto/index.tsx:
--------------------------------------------------------------------------------
1 | import { type Item, renderItem } from "@/app/cards-renderItem";
2 | import { DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3 | import { LegendList, type LegendListRef } from "@legendapp/list";
4 | import { useRef, useState } from "react";
5 | import { Button, Platform, StatusBar, StyleSheet, Text, View } from "react-native";
6 | import { TextInput } from "react-native-gesture-handler";
7 |
8 | interface CardsProps {
9 | numColumns?: number;
10 | }
11 |
12 | export default function Cards({ numColumns = 1 }: CardsProps) {
13 | const listRef = useRef(null);
14 |
15 | const [data, setData] = useState
- (
16 | () =>
17 | Array.from({ length: 1000 }, (_, i) => ({
18 | id: i.toString(),
19 | })) as any[],
20 | );
21 |
22 | const buttonText = useRef();
23 |
24 | return (
25 |
26 |
27 | {
34 | buttonText.current = text;
35 | }}
36 | />
37 |
48 | `id${item.id}`}
54 | estimatedItemSize={ESTIMATED_ITEM_LENGTH + 120}
55 | drawDistance={DRAW_DISTANCE}
56 | maintainVisibleContentPosition
57 | recycleItems={true}
58 | numColumns={numColumns}
59 | ListEmptyComponent={
60 |
61 | Empty
62 |
63 | }
64 | />
65 |
66 | );
67 | }
68 |
69 | const styles = StyleSheet.create({
70 | listHeader: {
71 | alignSelf: "center",
72 | height: 100,
73 | width: 100,
74 | backgroundColor: "#456AAA",
75 | borderRadius: 12,
76 | marginHorizontal: 8,
77 | marginVertical: 8,
78 | },
79 | listEmpty: {
80 | flex: 1,
81 | justifyContent: "center",
82 | alignItems: "center",
83 | backgroundColor: "#6789AB",
84 | paddingVertical: 16,
85 | },
86 | outerContainer: {
87 | backgroundColor: "#456",
88 | bottom: Platform.OS === "ios" ? 82 : 0,
89 | },
90 | scrollContainer: {},
91 | listContainer: {
92 | width: 400,
93 | maxWidth: "100%",
94 | marginHorizontal: "auto",
95 | },
96 | searchContainer: {
97 | padding: 8,
98 | backgroundColor: "#fff",
99 | borderBottomWidth: 1,
100 | borderBottomColor: "#e0e0e0",
101 | flexDirection: "row",
102 | justifyContent: "space-between",
103 | },
104 | searchInput: {
105 | height: 40,
106 | backgroundColor: "#f5f5f5",
107 | borderRadius: 8,
108 | paddingHorizontal: 12,
109 | fontSize: 16,
110 | flexGrow: 1,
111 | },
112 | container: {
113 | flex: 1,
114 | marginTop: StatusBar.currentHeight || 0,
115 | backgroundColor: "#f5f5f5",
116 | },
117 | });
118 |
--------------------------------------------------------------------------------
/example/app/add-to-end/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { useState } from "react";
3 | import { Button, SafeAreaView, StyleSheet, Text, View } from "react-native";
4 |
5 | const ListComponent = () => {
6 | const [items, setItems] = useState<{ id: string; title: string }[]>([]);
7 | const [counter, setCounter] = useState(0);
8 |
9 | const addSixtyItems = () => {
10 | const newItems = [];
11 | const startIndex = counter;
12 |
13 | for (let i = 0; i < 60; i++) {
14 | newItems.push({
15 | id: `item-${startIndex + i}`,
16 |
17 | title: `Item ${startIndex + i}`,
18 | });
19 | }
20 |
21 | setItems([...items, ...newItems]);
22 | setCounter((prev) => prev + 60);
23 | };
24 |
25 | const renderItem = ({ item }: { item: { id: string; title: string } }) => (
26 |
27 | {item.title}
28 |
29 | );
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | item.id}
40 | style={styles.list}
41 | contentContainerStyle={styles.listContent}
42 | maintainScrollAtEnd
43 | />
44 |
45 |
46 | );
47 | };
48 |
49 | const styles = StyleSheet.create({
50 | safeArea: {
51 | flex: 1,
52 | backgroundColor: "#f5f5f5",
53 | },
54 | container: {
55 | flex: 1,
56 | padding: 16,
57 | backgroundColor: "#f5f5f5",
58 | },
59 | list: {
60 | flex: 1,
61 | marginTop: 16,
62 | },
63 | listContent: {
64 | paddingBottom: 16,
65 | },
66 | itemContainer: {
67 | backgroundColor: "white",
68 | padding: 16,
69 | marginVertical: 8,
70 | borderRadius: 8,
71 | shadowColor: "#000",
72 | shadowOffset: { width: 0, height: 1 },
73 | shadowOpacity: 0.2,
74 | shadowRadius: 1,
75 | elevation: 2,
76 | },
77 | itemText: {
78 | fontSize: 16,
79 | },
80 | });
81 |
82 | export default ListComponent;
83 |
--------------------------------------------------------------------------------
/example/app/bidirectional-infinite-list/index.tsx:
--------------------------------------------------------------------------------
1 | import { type Item, renderItem } from "@/app/cards-renderItem";
2 | import { DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3 | import { LegendList, type LegendListRef } from "@legendapp/list";
4 | import { useRef, useState } from "react";
5 | import { RefreshControl, StyleSheet, View } from "react-native";
6 | import { useSafeAreaInsets } from "react-native-safe-area-context";
7 |
8 | let last = performance.now();
9 |
10 | export default function BidirectionalInfiniteList() {
11 | const listRef = useRef(null);
12 |
13 | const [data, setData] = useState
- (
14 | () =>
15 | Array.from({ length: 20 }, (_, i) => ({
16 | id: i.toString(),
17 | })) as any[],
18 | );
19 |
20 | const [refreshing, setRefreshing] = useState(false);
21 |
22 | const onRefresh = () => {
23 | console.log("onRefresh");
24 | setRefreshing(true);
25 | setTimeout(() => {
26 | setData((prevData) => {
27 | const initialIndex = Number.parseInt(prevData[0].id);
28 | const newData = [
29 | ...Array.from({ length: 5 }, (_, i) => ({
30 | id: (initialIndex - i - 1).toString(),
31 | })).reverse(),
32 | ...prevData,
33 | ];
34 | return newData;
35 | });
36 | setRefreshing(false);
37 | }, 500);
38 | };
39 |
40 | // useEffect(() => {
41 | // setTimeout(() => {
42 | // setData((prevData) => {
43 | // const initialIndex = Number.parseInt(prevData[0].id);
44 | // const newData = [
45 | // ...Array.from({ length: 1 }, (_, i) => ({
46 | // id: (initialIndex - i - 1).toString(),
47 | // })).reverse(),
48 | // ...prevData,
49 | // ];
50 | // return newData;
51 | // });
52 | // }, 2000);
53 | // }, []);
54 |
55 | const { bottom } = useSafeAreaInsets();
56 |
57 | return (
58 |
59 |
67 | }
68 | ref={listRef}
69 | initialScrollIndex={10}
70 | style={[StyleSheet.absoluteFill, styles.scrollContainer]}
71 | contentContainerStyle={styles.listContainer}
72 | data={data}
73 | renderItem={renderItem}
74 | keyExtractor={(item) => `id${item.id}`}
75 | estimatedItemSize={ESTIMATED_ITEM_LENGTH}
76 | drawDistance={DRAW_DISTANCE}
77 | maintainVisibleContentPosition
78 | recycleItems={true}
79 | ListFooterComponent={}
80 | onStartReached={(props) => {
81 | const time = performance.now();
82 | console.log("onStartReached", props, last - time);
83 | last = time;
84 | onRefresh();
85 | }}
86 | onEndReached={({ distanceFromEnd }) => {
87 | console.log("onEndReached", distanceFromEnd);
88 | if (distanceFromEnd > 0) {
89 | setTimeout(() => {
90 | setData((prevData) => {
91 | const newData = [
92 | ...prevData,
93 | ...Array.from({ length: 10 }, (_, i) => ({
94 | id: (Number.parseInt(prevData[prevData.length - 1].id) + i + 1).toString(),
95 | })),
96 | ];
97 | return newData;
98 | });
99 | }, 500);
100 | }
101 | }}
102 | />
103 |
104 | );
105 | }
106 |
107 | const styles = StyleSheet.create({
108 | listHeader: {
109 | alignSelf: "center",
110 | height: 100,
111 | width: 100,
112 | backgroundColor: "#456AAA",
113 | borderRadius: 12,
114 | marginHorizontal: 8,
115 | marginVertical: 8,
116 | },
117 | listEmpty: {
118 | flex: 1,
119 | justifyContent: "center",
120 | alignItems: "center",
121 | backgroundColor: "#6789AB",
122 | paddingVertical: 16,
123 | },
124 | outerContainer: {
125 | backgroundColor: "#456",
126 | },
127 | scrollContainer: {},
128 | listContainer: {
129 | width: 360,
130 | maxWidth: "100%",
131 | marginHorizontal: "auto",
132 | },
133 | });
134 |
--------------------------------------------------------------------------------
/example/app/cards-columns/index.tsx:
--------------------------------------------------------------------------------
1 | import Cards from "@/app/(tabs)/cards";
2 | import { LogBox, Platform, StyleSheet } from "react-native";
3 |
4 | LogBox.ignoreLogs(["Open debugger"]);
5 |
6 | export default function CardsColumns() {
7 | return ;
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | listHeader: {
12 | alignSelf: "center",
13 | height: 100,
14 | width: 100,
15 | backgroundColor: "#456AAA",
16 | borderRadius: 12,
17 | marginHorizontal: 8,
18 | marginVertical: 8,
19 | },
20 | listEmpty: {
21 | flex: 1,
22 | justifyContent: "center",
23 | alignItems: "center",
24 | backgroundColor: "#6789AB",
25 | paddingVertical: 16,
26 | },
27 | outerContainer: {
28 | backgroundColor: "#456",
29 | bottom: Platform.OS === "ios" ? 82 : 0,
30 | },
31 | scrollContainer: {},
32 | listContainer: {
33 | width: 400,
34 | maxWidth: "100%",
35 | marginHorizontal: "auto",
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/example/app/cards-flashlist/index.tsx:
--------------------------------------------------------------------------------
1 | import renderItem from "@/app/cards-renderItem";
2 | import { DO_SCROLL_TEST, DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH, RECYCLE_ITEMS } from "@/constants/constants";
3 | import { useScrollTest } from "@/constants/useScrollTest";
4 | import { FlashList, type ListRenderItemInfo } from "@shopify/flash-list";
5 | import { Fragment, useRef } from "react";
6 | import { StyleSheet, View } from "react-native";
7 |
8 | export default function HomeScreen() {
9 | const data = Array.from({ length: 1000 }, (_, i) => ({ id: i.toString() }));
10 |
11 | const scrollRef = useRef>(null);
12 |
13 | // useEffect(() => {
14 | // let amtPerInterval = 4;
15 | // let index = amtPerInterval;
16 | // const interval = setInterval(() => {
17 | // scrollRef.current?.scrollToIndex({
18 | // index,
19 | // });
20 | // index += amtPerInterval;
21 | // }, 100);
22 |
23 | // return () => clearInterval(interval);
24 | // });
25 |
26 | const renderItemFn = (info: ListRenderItemInfo) => {
27 | return RECYCLE_ITEMS ? renderItem(info) : {renderItem(info)};
28 | };
29 |
30 | if (DO_SCROLL_TEST) {
31 | useScrollTest((offset) => {
32 | scrollRef.current?.scrollToOffset({
33 | offset,
34 | animated: true,
35 | });
36 | });
37 | }
38 |
39 | return (
40 |
41 | item.id}
45 | contentContainerStyle={styles.listContainer}
46 | estimatedItemSize={ESTIMATED_ITEM_LENGTH}
47 | drawDistance={DRAW_DISTANCE}
48 | ref={scrollRef}
49 | ListHeaderComponent={}
50 | ListHeaderComponentStyle={styles.listHeader}
51 | />
52 |
53 | );
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | listHeader: {
58 | alignSelf: "center",
59 | height: 100,
60 | width: 100,
61 | backgroundColor: "#456AAA",
62 | borderRadius: 12,
63 | marginHorizontal: 8,
64 | marginTop: 8,
65 | },
66 | outerContainer: {
67 | backgroundColor: "#456",
68 | },
69 | scrollContainer: {
70 | // paddingHorizontal: 8,
71 | },
72 | titleContainer: {
73 | flexDirection: "row",
74 | alignItems: "center",
75 | gap: 8,
76 | },
77 | stepContainer: {
78 | gap: 8,
79 | marginBottom: 8,
80 | },
81 | reactLogo: {
82 | height: 178,
83 | width: 290,
84 | bottom: 0,
85 | left: 0,
86 | position: "absolute",
87 | },
88 | itemContainer: {
89 | // padding: 4,
90 | // borderBottomWidth: 1,
91 | // borderBottomColor: "#ccc",
92 | },
93 | listContainer: {
94 | //paddingHorizontal: 16,
95 | //paddingTop: 48,
96 | },
97 | itemTitle: {
98 | fontSize: 18,
99 | fontWeight: "bold",
100 | marginBottom: 8,
101 | color: "#1a1a1a",
102 | },
103 | itemBody: {
104 | fontSize: 14,
105 | color: "#666666",
106 | lineHeight: 20,
107 | flex: 1,
108 | },
109 | itemFooter: {
110 | flexDirection: "row",
111 | justifyContent: "flex-start",
112 | gap: 16,
113 | marginTop: 12,
114 | paddingTop: 12,
115 | borderTopWidth: 1,
116 | borderTopColor: "#f0f0f0",
117 | },
118 | footerText: {
119 | fontSize: 14,
120 | color: "#888888",
121 | },
122 | });
123 |
--------------------------------------------------------------------------------
/example/app/cards-flatlist/index.tsx:
--------------------------------------------------------------------------------
1 | import renderItem from "@/app/cards-renderItem";
2 | import { FlatList, StyleSheet, View } from "react-native";
3 |
4 | export default function CardsFlatList() {
5 | const data = Array.from({ length: 1000 }, (_, i) => ({ id: i.toString() }));
6 |
7 | return (
8 |
9 | item.id}
14 | contentContainerStyle={styles.listContainer}
15 | ListHeaderComponent={}
16 | ListHeaderComponentStyle={styles.listHeader}
17 | // Performance optimizations
18 | windowSize={3} // Reduced window size for better performance
19 | maxToRenderPerBatch={5} // Reduced batch size for smoother scrolling
20 | initialNumToRender={8} // Initial render amount
21 | removeClippedSubviews={true} // Detach views outside of the viewport
22 | updateCellsBatchingPeriod={50} // Batching period for updates
23 | />
24 |
25 | );
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | listHeader: {
30 | alignSelf: "center",
31 | height: 100,
32 | width: 100,
33 | backgroundColor: "#456AAA",
34 | borderRadius: 12,
35 | marginHorizontal: 8,
36 | marginTop: 8,
37 | },
38 | outerContainer: {
39 | backgroundColor: "#456",
40 | },
41 | scrollContainer: {
42 | // paddingHorizontal: 16,
43 | },
44 | titleContainer: {
45 | flexDirection: "row",
46 | alignItems: "center",
47 | gap: 8,
48 | },
49 | stepContainer: {
50 | gap: 8,
51 | marginBottom: 8,
52 | },
53 | reactLogo: {
54 | height: 178,
55 | width: 290,
56 | bottom: 0,
57 | left: 0,
58 | position: "absolute",
59 | },
60 | itemContainer: {
61 | // padding: 4,
62 | // borderBottomWidth: 1,
63 | // borderBottomColor: "#ccc",
64 | },
65 | listContainer: {
66 | paddingHorizontal: 16,
67 | // paddingTop: 48,
68 | },
69 | itemTitle: {
70 | fontSize: 18,
71 | fontWeight: "bold",
72 | marginBottom: 8,
73 | color: "#1a1a1a",
74 | },
75 | itemBody: {
76 | fontSize: 14,
77 | color: "#666666",
78 | lineHeight: 20,
79 | flex: 1,
80 | },
81 | itemFooter: {
82 | flexDirection: "row",
83 | justifyContent: "flex-start",
84 | gap: 16,
85 | marginTop: 12,
86 | paddingTop: 12,
87 | borderTopWidth: 1,
88 | borderTopColor: "#f0f0f0",
89 | },
90 | footerText: {
91 | fontSize: 14,
92 | color: "#888888",
93 | },
94 | });
95 |
--------------------------------------------------------------------------------
/example/app/cards-no-recycle/index.tsx:
--------------------------------------------------------------------------------
1 | import { type Item, renderItem } from "@/app/cards-renderItem";
2 | import { DO_SCROLL_TEST, DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3 | import { useScrollTest } from "@/constants/useScrollTest";
4 | import { LegendList, type LegendListRef } from "@legendapp/list";
5 | import { useRef, useState } from "react";
6 | import { LogBox, Platform, StyleSheet, Text, View } from "react-native";
7 |
8 | LogBox.ignoreLogs(["Open debugger"]);
9 |
10 | interface CardsProps {
11 | numColumns?: number;
12 | }
13 |
14 | export default function Cards({ numColumns = 1 }: CardsProps) {
15 | const listRef = useRef(null);
16 |
17 | const [data, setData] = useState
- (
18 | () =>
19 | Array.from({ length: 1000 }, (_, i) => ({
20 | id: i.toString(),
21 | })) as any[],
22 | );
23 |
24 | if (DO_SCROLL_TEST) {
25 | useScrollTest((offset) => {
26 | listRef.current?.scrollToOffset({
27 | offset: offset,
28 | animated: true,
29 | });
30 | });
31 | }
32 |
33 | return (
34 |
35 | `id${item.id}`}
42 | estimatedItemSize={ESTIMATED_ITEM_LENGTH}
43 | drawDistance={DRAW_DISTANCE}
44 | recycleItems={false}
45 | numColumns={numColumns}
46 | // initialScrollIndex={50}
47 | // alignItemsAtEnd
48 | // maintainScrollAtEnd
49 | // onEndReached={({ distanceFromEnd }) => {
50 | // console.log("onEndReached", distanceFromEnd);
51 | // }}
52 | ListHeaderComponent={}
53 | ListHeaderComponentStyle={styles.listHeader}
54 | ListFooterComponent={}
55 | ListFooterComponentStyle={styles.listHeader}
56 | ListEmptyComponent={
57 |
58 | Empty
59 |
60 | }
61 | // viewabilityConfigCallbackPairs={[
62 | // {
63 | // viewabilityConfig: { id: "viewability", viewAreaCoveragePercentThreshold: 50 },
64 | // // onViewableItemsChanged: ({ viewableItems, changed }) => {
65 | // // console.log(
66 | // // 'onViewableItems',
67 | // // viewableItems.map((v) => v.key),
68 | // // );
69 | // // // console.log('onViewableChanged', changed);
70 | // // },
71 | // },
72 | // ]}
73 |
74 | // initialScrollOffset={20000}
75 | // initialScrollIndex={500}
76 | // inverted
77 | // horizontal
78 | />
79 |
80 | );
81 | }
82 |
83 | const styles = StyleSheet.create({
84 | listHeader: {
85 | alignSelf: "center",
86 | height: 100,
87 | width: 100,
88 | backgroundColor: "#456AAA",
89 | borderRadius: 12,
90 | marginHorizontal: 8,
91 | marginVertical: 8,
92 | },
93 | listEmpty: {
94 | flex: 1,
95 | justifyContent: "center",
96 | alignItems: "center",
97 | backgroundColor: "#6789AB",
98 | paddingVertical: 16,
99 | },
100 | outerContainer: {
101 | backgroundColor: "#456",
102 | bottom: Platform.OS === "ios" ? 82 : 0,
103 | },
104 | scrollContainer: {},
105 | listContainer: {
106 | width: 400,
107 | maxWidth: "100%",
108 | marginHorizontal: "auto",
109 | },
110 | });
111 |
--------------------------------------------------------------------------------
/example/app/chat-example/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { useHeaderHeight } from "@react-navigation/elements";
3 | import { useState } from "react";
4 | import { Button, KeyboardAvoidingView, Platform, StyleSheet, Text, TextInput, View } from "react-native";
5 | import { SafeAreaView } from "react-native-safe-area-context";
6 |
7 | type Message = {
8 | id: string;
9 | text: string;
10 | sender: "user" | "bot";
11 | timeStamp: number;
12 | };
13 |
14 | let idCounter = 0;
15 | const MS_PER_SECOND = 1000;
16 |
17 | const defaultChatMessages: Message[] = [
18 | {
19 | id: String(idCounter++),
20 | text: "Hi, I have a question",
21 | sender: "user",
22 | timeStamp: Date.now() - MS_PER_SECOND * 5,
23 | },
24 | { id: String(idCounter++), text: "Hello", sender: "bot", timeStamp: Date.now() - MS_PER_SECOND * 4 },
25 | { id: String(idCounter++), text: "How can I help you?", sender: "bot", timeStamp: Date.now() - MS_PER_SECOND * 3 },
26 | ];
27 |
28 | const ChatExample = () => {
29 | const [messages, setMessages] = useState(defaultChatMessages);
30 | const [inputText, setInputText] = useState("");
31 | const headerHeight = Platform.OS === "ios" ? useHeaderHeight() : 80;
32 |
33 | const sendMessage = () => {
34 | const text = inputText || "Empty message";
35 | if (text.trim()) {
36 | setMessages((messages) => [
37 | ...messages,
38 | { id: String(idCounter++), text: text, sender: "user", timeStamp: Date.now() },
39 | ]);
40 | setInputText("");
41 | setTimeout(() => {
42 | setMessages((messages) => [
43 | ...messages,
44 | {
45 | id: String(idCounter++),
46 | text: `Answer: ${text.toUpperCase()}`,
47 | sender: "bot",
48 | timeStamp: Date.now(),
49 | },
50 | ]);
51 | }, 300);
52 | }
53 | };
54 |
55 | return (
56 |
57 |
63 | item.id}
67 | estimatedItemSize={10} // A size that's way too small to check the behavior is correct
68 | initialScrollIndex={messages.length - 1}
69 | maintainVisibleContentPosition
70 | maintainScrollAtEnd
71 | alignItemsAtEnd
72 | renderItem={({ item }) => (
73 | <>
74 |
81 |
82 | {item.text}
83 |
84 |
85 |
88 |
89 | {new Date(item.timeStamp).toLocaleTimeString()}
90 |
91 |
92 | >
93 | )}
94 | />
95 |
96 |
102 |
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | const styles = StyleSheet.create({
110 | container: {
111 | flex: 1,
112 | backgroundColor: "#fff",
113 | },
114 | contentContainer: {
115 | paddingHorizontal: 16,
116 | },
117 | messageContainer: {
118 | padding: 16,
119 | borderRadius: 16,
120 | marginVertical: 4,
121 | },
122 | messageText: {
123 | fontSize: 16,
124 | },
125 | userMessageText: {
126 | color: "white",
127 | },
128 | inputContainer: {
129 | flexDirection: "row",
130 | alignItems: "center",
131 | padding: 10,
132 | borderTopWidth: 1,
133 | borderColor: "#ccc",
134 | },
135 | botMessageContainer: {
136 | backgroundColor: "#f1f1f1",
137 | },
138 | userMessageContainer: {
139 | backgroundColor: "#007AFF",
140 | },
141 | botStyle: {
142 | maxWidth: "75%",
143 | alignSelf: "flex-start",
144 | },
145 | userStyle: {
146 | maxWidth: "75%",
147 | alignSelf: "flex-end",
148 | alignItems: "flex-end",
149 | },
150 | input: {
151 | flex: 1,
152 | borderWidth: 1,
153 | borderColor: "#ccc",
154 | borderRadius: 5,
155 | padding: 10,
156 | marginRight: 10,
157 | },
158 | timeStamp: {
159 | marginVertical: 5,
160 | },
161 | timeStampText: {
162 | fontSize: 12,
163 | color: "#888",
164 | },
165 | });
166 |
167 | export default ChatExample;
168 |
--------------------------------------------------------------------------------
/example/app/chat-keyboard/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list/keyboard-controller";
2 | import { AnimatedLegendList } from "@legendapp/list/reanimated";
3 | import { useHeaderHeight } from "@react-navigation/elements";
4 | import { useState } from "react";
5 | import { Button, Platform, StyleSheet, Text, TextInput, View } from "react-native";
6 | import { KeyboardAvoidingView, KeyboardProvider } from "react-native-keyboard-controller";
7 |
8 | type Message = {
9 | id: string;
10 | text: string;
11 | sender: "user" | "bot";
12 | timeStamp: number;
13 | };
14 |
15 | let idCounter = 0;
16 | const MS_PER_SECOND = 1000;
17 |
18 | const defaultChatMessages: Message[] = [
19 | {
20 | id: String(idCounter++),
21 | text: "Hi, I have a question about your product",
22 | sender: "user",
23 | timeStamp: Date.now() - MS_PER_SECOND * 5,
24 | },
25 | {
26 | id: String(idCounter++),
27 | text: "Hello there! How can I assist you today?",
28 | sender: "bot",
29 | timeStamp: Date.now() - MS_PER_SECOND * 4,
30 | },
31 | {
32 | id: String(idCounter++),
33 | text: "I'm looking for information about pricing plans",
34 | sender: "user",
35 | timeStamp: Date.now() - MS_PER_SECOND * 4,
36 | },
37 | {
38 | id: String(idCounter++),
39 | text: "We offer several pricing tiers based on your needs",
40 | sender: "bot",
41 | timeStamp: Date.now() - MS_PER_SECOND * 4,
42 | },
43 | {
44 | id: String(idCounter++),
45 | text: "Our basic plan starts at $9.99 per month",
46 | sender: "bot",
47 | timeStamp: Date.now() - MS_PER_SECOND * 4,
48 | },
49 | {
50 | id: String(idCounter++),
51 | text: "Do you offer any discounts for annual billing?",
52 | sender: "user",
53 | timeStamp: Date.now() - MS_PER_SECOND * 4,
54 | },
55 | {
56 | id: String(idCounter++),
57 | text: "Yes! You can save 20% with our annual billing option",
58 | sender: "bot",
59 | timeStamp: Date.now() - MS_PER_SECOND * 4,
60 | },
61 | {
62 | id: String(idCounter++),
63 | text: "That sounds great. What features are included?",
64 | sender: "user",
65 | timeStamp: Date.now() - MS_PER_SECOND * 4,
66 | },
67 | {
68 | id: String(idCounter++),
69 | text: "The basic plan includes all core features plus 10GB storage",
70 | sender: "bot",
71 | timeStamp: Date.now() - MS_PER_SECOND * 4,
72 | },
73 | {
74 | id: String(idCounter++),
75 | text: "Premium plans include priority support and additional tools",
76 | sender: "bot",
77 | timeStamp: Date.now() - MS_PER_SECOND * 4,
78 | },
79 | {
80 | id: String(idCounter++),
81 | text: "I think the basic plan would work for my needs",
82 | sender: "user",
83 | timeStamp: Date.now() - MS_PER_SECOND * 4,
84 | },
85 | {
86 | id: String(idCounter++),
87 | text: "Perfect! I can help you get set up with that",
88 | sender: "bot",
89 | timeStamp: Date.now() - MS_PER_SECOND * 4,
90 | },
91 | {
92 | id: String(idCounter++),
93 | text: "Thanks for your help so far",
94 | sender: "user",
95 | timeStamp: Date.now() - MS_PER_SECOND * 4,
96 | },
97 | {
98 | id: String(idCounter++),
99 | text: "You're welcome! Is there anything else I can assist with today?",
100 | sender: "bot",
101 | timeStamp: Date.now() - MS_PER_SECOND * 3,
102 | },
103 | ];
104 |
105 | const ChatExample = () => {
106 | const [messages, setMessages] = useState(defaultChatMessages);
107 | const [inputText, setInputText] = useState("");
108 | const headerHeight = Platform.OS === "ios" ? useHeaderHeight() : 56;
109 |
110 | const sendMessage = () => {
111 | const text = inputText || "Empty message";
112 | if (text.trim()) {
113 | setMessages((messages) => [
114 | ...messages,
115 | { id: String(idCounter++), text: text, sender: "user", timeStamp: Date.now() },
116 | ]);
117 | setInputText("");
118 | setTimeout(() => {
119 | setMessages((messages) => [
120 | ...messages,
121 | {
122 | id: String(idCounter++),
123 | text: `Answer: ${text.toUpperCase()}`,
124 | sender: "bot",
125 | timeStamp: Date.now(),
126 | },
127 | ]);
128 | }, 300);
129 | }
130 | };
131 |
132 | // Note: There's something weird with the SafeAreaView interacting with the KeyboardAvoidingView here I think,
133 | // so there's some weird margins going on...
134 |
135 | return (
136 |
137 |
143 | item.id}
147 | estimatedItemSize={80}
148 | LegendList={AnimatedLegendList}
149 | maintainScrollAtEnd
150 | alignItemsAtEnd
151 | initialScrollIndex={messages.length - 1}
152 | maintainVisibleContentPosition
153 | renderItem={({ item }) => (
154 | <>
155 |
162 |
163 | {item.text}
164 |
165 |
166 |
169 |
170 | {new Date(item.timeStamp).toLocaleTimeString()}
171 |
172 |
173 | >
174 | )}
175 | />
176 |
177 |
183 |
184 |
185 |
186 |
187 | );
188 | };
189 |
190 | const styles = StyleSheet.create({
191 | container: {
192 | flex: 1,
193 | backgroundColor: "#fff",
194 | },
195 | contentContainer: {
196 | paddingHorizontal: 16,
197 | },
198 | messageContainer: {
199 | padding: 16,
200 | borderRadius: 16,
201 | marginVertical: 4,
202 | },
203 | messageText: {
204 | fontSize: 16,
205 | },
206 | userMessageText: {
207 | color: "white",
208 | },
209 | inputContainer: {
210 | flexDirection: "row",
211 | alignItems: "center",
212 | padding: 10,
213 | borderTopWidth: 1,
214 | borderColor: "#ccc",
215 | },
216 | botMessageContainer: {
217 | backgroundColor: "#f1f1f1",
218 | },
219 | userMessageContainer: {
220 | backgroundColor: "#007AFF",
221 | },
222 | botStyle: {
223 | maxWidth: "75%",
224 | alignSelf: "flex-start",
225 | },
226 | userStyle: {
227 | maxWidth: "75%",
228 | alignSelf: "flex-end",
229 | alignItems: "flex-end",
230 | },
231 | input: {
232 | flex: 1,
233 | borderWidth: 1,
234 | borderColor: "#ccc",
235 | borderRadius: 5,
236 | padding: 10,
237 | marginRight: 10,
238 | },
239 | timeStamp: {
240 | marginVertical: 5,
241 | },
242 | timeStampText: {
243 | fontSize: 12,
244 | color: "#888",
245 | },
246 | });
247 |
248 | export default ChatExample;
249 |
--------------------------------------------------------------------------------
/example/app/columns/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { useState } from "react";
3 | import { useEffect } from "react";
4 | import { LogBox, StyleSheet, Text, View } from "react-native";
5 |
6 | LogBox.ignoreLogs(["Open debugger"]);
7 |
8 | const initialData = Array.from({ length: 8 }, (_, index) => ({ id: index.toString() }));
9 |
10 | export default function Columns() {
11 | const [data, setData] = useState(initialData);
12 |
13 | useEffect(() => {
14 | setTimeout(() => {
15 | setData(Array.from({ length: 20 }, (_, index) => ({ id: index.toString() })));
16 | }, 1000);
17 | });
18 |
19 | return (
20 |
21 | item.id}
25 | numColumns={3}
26 | columnWrapperStyle={{
27 | columnGap: 16,
28 | rowGap: 16,
29 | }}
30 | />
31 |
32 | );
33 | }
34 |
35 | function Item({ item }: { item: { id: string } }) {
36 | return (
37 |
38 |
39 | Item {item.id}
40 |
41 | );
42 | }
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | flex: 1,
47 | backgroundColor: "#fff",
48 | },
49 | redRectangle: {
50 | aspectRatio: 1,
51 | },
52 | redRectangleInner: {
53 | height: "100%",
54 | width: "100%",
55 | backgroundColor: "red",
56 | borderRadius: 8,
57 | },
58 | columnWrapper: {
59 | justifyContent: "space-between",
60 | },
61 | listHeader: {
62 | alignSelf: "center",
63 | height: 100,
64 | width: 100,
65 | backgroundColor: "#456AAA",
66 | borderRadius: 12,
67 | marginHorizontal: 8,
68 | marginVertical: 8,
69 | },
70 | listEmpty: {
71 | flex: 1,
72 | justifyContent: "center",
73 | alignItems: "center",
74 | backgroundColor: "#6789AB",
75 | paddingVertical: 16,
76 | height: 100,
77 | },
78 | });
79 |
--------------------------------------------------------------------------------
/example/app/countries-flashlist/index.tsx:
--------------------------------------------------------------------------------
1 | import { FlashList } from "@shopify/flash-list";
2 | import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
3 | import { useMemo, useState } from "react";
4 | import { Pressable, StatusBar, StyleSheet, Text, TextInput, View } from "react-native";
5 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
6 |
7 | export const unstable_settings = {
8 | initialRouteName: "index",
9 | };
10 |
11 | export const createTitle = () => "Countries";
12 |
13 | type Country = {
14 | id: string;
15 | name: string;
16 | flag: string;
17 | };
18 |
19 | // Convert countries object to array and add an id
20 | const DATA: Country[] = Object.entries(countries)
21 | .map(([code, country]) => ({
22 | id: code,
23 | name: country.name,
24 | flag: getEmojiFlag(code as TCountryCode),
25 | }))
26 | .sort((a, b) => a.name.localeCompare(b.name));
27 |
28 | type ItemProps = {
29 | item: Country;
30 | onPress: () => void;
31 | isSelected: boolean;
32 | };
33 |
34 | const Item = ({ item, onPress, isSelected }: ItemProps) => (
35 | [styles.item, isSelected && styles.selectedItem, pressed && styles.pressedItem]}
38 | >
39 |
40 | {item.flag}
41 |
42 |
43 |
44 | {item.name}
45 | ({item.id})
46 |
47 |
48 |
49 | );
50 |
51 | const App = () => {
52 | const [selectedId, setSelectedId] = useState();
53 | const [searchQuery, setSearchQuery] = useState("");
54 |
55 | const filteredData = useMemo(() => {
56 | const query = searchQuery.toLowerCase();
57 | return DATA.filter(
58 | (country) => country.name.toLowerCase().includes(query) || country.id.toLowerCase().includes(query),
59 | );
60 | }, [searchQuery]);
61 |
62 | const renderItem = ({ item }: { item: Country }) => {
63 | const isSelected = item.id === selectedId;
64 | return
- setSelectedId(item.id)} isSelected={isSelected} />;
65 | };
66 |
67 | return (
68 |
69 |
70 |
71 |
80 |
81 | item.id}
85 | extraData={selectedId}
86 | estimatedItemSize={70}
87 | //scrollEventThrottle={200}
88 | disableAutoLayout
89 | />
90 |
91 |
92 | );
93 | };
94 |
95 | export default App;
96 |
97 | const styles = StyleSheet.create({
98 | container: {
99 | flex: 1,
100 | marginTop: StatusBar.currentHeight || 0,
101 | backgroundColor: "#f5f5f5",
102 | },
103 | searchContainer: {
104 | padding: 8,
105 | backgroundColor: "#fff",
106 | borderBottomWidth: 1,
107 | borderBottomColor: "#e0e0e0",
108 | },
109 | searchInput: {
110 | height: 40,
111 | backgroundColor: "#f5f5f5",
112 | borderRadius: 8,
113 | paddingHorizontal: 12,
114 | fontSize: 16,
115 | },
116 | item: {
117 | paddingHorizontal: 16,
118 | paddingVertical: 6,
119 | flexDirection: "row",
120 | alignItems: "center",
121 | backgroundColor: "#fff",
122 | borderRadius: 12,
123 | // elevation: 3,
124 | },
125 | selectedItem: {
126 | // backgroundColor: "#e3f2fd",
127 | // borderColor: "#1976d2",
128 | // borderWidth: 1,
129 | },
130 | pressedItem: {
131 | // backgroundColor: "#f0f0f0",
132 | },
133 | flagContainer: {
134 | marginRight: 16,
135 | width: 40,
136 | height: 40,
137 | borderRadius: 20,
138 | backgroundColor: "#f8f9fa",
139 | alignItems: "center",
140 | justifyContent: "center",
141 | },
142 | flag: {
143 | fontSize: 28,
144 | },
145 | contentContainer: {
146 | flex: 1,
147 | justifyContent: "center",
148 | },
149 | title: {
150 | fontSize: 16,
151 | color: "#333",
152 | fontWeight: "500",
153 | },
154 | selectedText: {
155 | color: "#1976d2",
156 | fontWeight: "600",
157 | },
158 | countryCode: {
159 | fontSize: 14,
160 | color: "#666",
161 | fontWeight: "400",
162 | },
163 | });
164 |
--------------------------------------------------------------------------------
/example/app/countries-reorder/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
3 | import { useMemo, useState } from "react";
4 | import { Pressable, StatusBar, StyleSheet, Text, View } from "react-native";
5 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
6 |
7 | export const unstable_settings = {
8 | initialRouteName: "index",
9 | };
10 |
11 | export const createTitle = () => "Countries";
12 |
13 | type Country = {
14 | id: string;
15 | name: string;
16 | flag: string;
17 | };
18 |
19 | // Convert countries object to array and add an id
20 | const DATA: Country[] = Object.entries(countries)
21 | // .slice(0, 5)
22 | .map(([code, country]) => ({
23 | id: code,
24 | name: country.name,
25 | flag: getEmojiFlag(code as TCountryCode),
26 | }))
27 | .sort((a, b) => a.name.localeCompare(b.name));
28 |
29 | type ItemProps = {
30 | item: Country;
31 | onPress: () => void;
32 | isSelected: boolean;
33 | };
34 |
35 | const Item = ({ item, onPress, isSelected }: ItemProps) => (
36 | [styles.item, isSelected && styles.selectedItem, pressed && styles.pressedItem]}
39 | >
40 |
41 | {item.flag}
42 |
43 |
44 |
45 | {item.name}
46 | ({item.id})
47 |
48 |
49 |
50 | );
51 |
52 | const App = () => {
53 | const [selectedId, setSelectedId] = useState();
54 | const [randomSeed, setRandomSeed] = useState(0);
55 |
56 | // Display either ordered or randomized data based on state
57 | const displayData = useMemo(() => {
58 | if (randomSeed) {
59 | // Randomize the order
60 | return [...DATA].sort(() => Math.random() - 0.5);
61 | }
62 | // Return alphabetically sorted data
63 | return [...DATA].sort((a, b) => a.name.localeCompare(b.name));
64 | }, [randomSeed]);
65 |
66 | const renderItem = ({ item }: { item: Country }) => {
67 | const isSelected = item.id === selectedId;
68 | return
- setSelectedId(item.id)} isSelected={isSelected} />;
69 | };
70 |
71 | return (
72 |
73 |
74 |
75 | setRandomSeed(randomSeed + 1)}>
76 | Randomize Order
77 |
78 |
79 | item.id}
83 | extraData={selectedId}
84 | estimatedItemSize={70}
85 | recycleItems
86 | onStartReachedThreshold={0.1}
87 | onStartReached={({ distanceFromStart }) => {
88 | console.log("onStartReached", distanceFromStart);
89 | }}
90 | onEndReachedThreshold={0.1}
91 | onEndReached={({ distanceFromEnd }) => {
92 | console.log("onEndReached", distanceFromEnd);
93 | }}
94 | // ListHeaderComponent={}
95 | // ListFooterComponent={}
96 | ItemSeparatorComponent={Separator}
97 | />
98 |
99 |
100 | );
101 | };
102 |
103 | const Separator = () => ;
104 |
105 | export default App;
106 |
107 | const styles = StyleSheet.create({
108 | container: {
109 | flex: 1,
110 | marginTop: StatusBar.currentHeight || 0,
111 | backgroundColor: "#f5f5f5",
112 | },
113 | headerContainer: {
114 | padding: 8,
115 | backgroundColor: "#fff",
116 | borderBottomWidth: 1,
117 | borderBottomColor: "#e0e0e0",
118 | alignItems: "center",
119 | },
120 | reorderButton: {
121 | backgroundColor: "#4a86e8",
122 | paddingVertical: 10,
123 | paddingHorizontal: 20,
124 | borderRadius: 8,
125 | width: "80%",
126 | alignItems: "center",
127 | },
128 | buttonText: {
129 | color: "#fff",
130 | fontSize: 16,
131 | fontWeight: "500",
132 | },
133 | item: {
134 | paddingHorizontal: 16,
135 | paddingVertical: 6,
136 | flexDirection: "row",
137 | alignItems: "center",
138 | backgroundColor: "#fff",
139 | borderRadius: 12,
140 | // shadowColor: "#000",
141 | // shadowOffset: {
142 | // width: 0,
143 | // height: 2,
144 | // },
145 | // shadowOpacity: 0.1,
146 | // shadowRadius: 3,
147 | // elevation: 3,
148 | },
149 | selectedItem: {
150 | // backgroundColor: "#e3f2fd",
151 | // borderColor: "#1976d2",
152 | // borderWidth: 1,
153 | },
154 | pressedItem: {
155 | // backgroundColor: "#f0f0f0",
156 | },
157 | flagContainer: {
158 | marginRight: 16,
159 | width: 40,
160 | height: 40,
161 | borderRadius: 20,
162 | backgroundColor: "#f8f9fa",
163 | alignItems: "center",
164 | justifyContent: "center",
165 | },
166 | flag: {
167 | fontSize: 28,
168 | },
169 | contentContainer: {
170 | flex: 1,
171 | justifyContent: "center",
172 | },
173 | title: {
174 | fontSize: 16,
175 | color: "#333",
176 | fontWeight: "500",
177 | },
178 | selectedText: {
179 | color: "#1976d2",
180 | fontWeight: "600",
181 | },
182 | countryCode: {
183 | fontSize: 14,
184 | color: "#666",
185 | fontWeight: "400",
186 | },
187 | });
188 |
--------------------------------------------------------------------------------
/example/app/countries/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
3 | import { useMemo, useState } from "react";
4 | import { Pressable, StatusBar, StyleSheet, Text, TextInput, View } from "react-native";
5 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
6 |
7 | export const unstable_settings = {
8 | initialRouteName: "index",
9 | };
10 |
11 | export const createTitle = () => "Countries";
12 |
13 | type Country = {
14 | id: string;
15 | name: string;
16 | flag: string;
17 | };
18 |
19 | // Convert countries object to array and add an id
20 | const DATA: Country[] = Object.entries(countries)
21 | .map(([code, country]) => ({
22 | id: code,
23 | name: country.name,
24 | flag: getEmojiFlag(code as TCountryCode),
25 | }))
26 | .sort((a, b) => a.name.localeCompare(b.name));
27 |
28 | type ItemProps = {
29 | item: Country;
30 | onPress: () => void;
31 | isSelected: boolean;
32 | };
33 |
34 | const Item = ({ item, onPress, isSelected }: ItemProps) => (
35 | [styles.item, isSelected && styles.selectedItem, pressed && styles.pressedItem]}
38 | >
39 |
40 | {item.flag}
41 |
42 |
43 |
44 | {item.name}
45 | ({item.id})
46 |
47 |
48 |
49 | );
50 |
51 | const App = () => {
52 | const [selectedId, setSelectedId] = useState();
53 | const [searchQuery, setSearchQuery] = useState("");
54 |
55 | const filteredData = useMemo(() => {
56 | const query = searchQuery.toLowerCase();
57 | return DATA.filter(
58 | (country) => country.name.toLowerCase().includes(query) || country.id.toLowerCase().includes(query),
59 | );
60 | }, [searchQuery]);
61 |
62 | const renderItem = ({ item }: { item: Country }) => {
63 | const isSelected = item.id === selectedId;
64 | return
- setSelectedId(item.id)} isSelected={isSelected} />;
65 | };
66 |
67 | return (
68 |
69 |
70 |
71 |
80 |
81 | item.id}
85 | extraData={selectedId}
86 | estimatedItemSize={70}
87 | recycleItems
88 | onStartReachedThreshold={0.1}
89 | onStartReached={({ distanceFromStart }) => {
90 | console.log("onStartReached", distanceFromStart);
91 | }}
92 | onEndReachedThreshold={0.1}
93 | onEndReached={({ distanceFromEnd }) => {
94 | console.log("onEndReached", distanceFromEnd);
95 | }}
96 | // ListHeaderComponent={}
97 | // ListFooterComponent={}
98 | // ItemSeparatorComponent={Separator}
99 | />
100 |
101 |
102 | );
103 | };
104 |
105 | const Separator = () => ;
106 |
107 | export default App;
108 |
109 | const styles = StyleSheet.create({
110 | container: {
111 | flex: 1,
112 | marginTop: StatusBar.currentHeight || 0,
113 | backgroundColor: "#f5f5f5",
114 | },
115 | searchContainer: {
116 | padding: 8,
117 | backgroundColor: "#fff",
118 | borderBottomWidth: 1,
119 | borderBottomColor: "#e0e0e0",
120 | },
121 | searchInput: {
122 | height: 40,
123 | backgroundColor: "#f5f5f5",
124 | borderRadius: 8,
125 | paddingHorizontal: 12,
126 | fontSize: 16,
127 | },
128 | item: {
129 | paddingHorizontal: 16,
130 | paddingVertical: 6,
131 | flexDirection: "row",
132 | alignItems: "center",
133 | backgroundColor: "#fff",
134 | borderRadius: 12,
135 | // shadowColor: "#000",
136 | // shadowOffset: {
137 | // width: 0,
138 | // height: 2,
139 | // },
140 | // shadowOpacity: 0.1,
141 | // shadowRadius: 3,
142 | // elevation: 3,
143 | },
144 | selectedItem: {
145 | // backgroundColor: "#e3f2fd",
146 | // borderColor: "#1976d2",
147 | // borderWidth: 1,
148 | },
149 | pressedItem: {
150 | // backgroundColor: "#f0f0f0",
151 | },
152 | flagContainer: {
153 | marginRight: 16,
154 | width: 40,
155 | height: 40,
156 | borderRadius: 20,
157 | backgroundColor: "#f8f9fa",
158 | alignItems: "center",
159 | justifyContent: "center",
160 | },
161 | flag: {
162 | fontSize: 28,
163 | },
164 | contentContainer: {
165 | flex: 1,
166 | justifyContent: "center",
167 | },
168 | title: {
169 | fontSize: 16,
170 | color: "#333",
171 | fontWeight: "500",
172 | },
173 | selectedText: {
174 | color: "#1976d2",
175 | fontWeight: "600",
176 | },
177 | countryCode: {
178 | fontSize: 14,
179 | color: "#666",
180 | fontWeight: "400",
181 | },
182 | });
183 |
--------------------------------------------------------------------------------
/example/app/extra-data/index.tsx:
--------------------------------------------------------------------------------
1 | import { LegendList } from "@legendapp/list";
2 | import { useState } from "react";
3 | import { StatusBar, StyleSheet, Text, TouchableOpacity } from "react-native";
4 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5 |
6 | type ItemData = {
7 | id: string;
8 | title: string;
9 | };
10 |
11 | const DATA: ItemData[] = [
12 | {
13 | id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
14 | title: "First Item",
15 | },
16 | {
17 | id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
18 | title: "Second Item",
19 | },
20 | {
21 | id: "58694a0f-3da1-471f-bd96-145571e29d72",
22 | title: "Third Item",
23 | },
24 | ];
25 |
26 | type ItemProps = {
27 | item: ItemData;
28 | onPress: () => void;
29 | backgroundColor: string;
30 | textColor: string;
31 | };
32 |
33 | const Item = ({ item, onPress, backgroundColor, textColor }: ItemProps) => (
34 |
35 | {item.title}
36 |
37 | );
38 |
39 | const App = () => {
40 | const [selectedId, setSelectedId] = useState();
41 |
42 | const renderItem = ({ item }: { item: ItemData }) => {
43 | const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
44 | const color = item.id === selectedId ? "white" : "black";
45 |
46 | return (
47 |
- setSelectedId(item.id)}
50 | backgroundColor={backgroundColor}
51 | textColor={color}
52 | />
53 | );
54 | };
55 |
56 | return (
57 |
58 |
59 | item.id}
63 | extraData={selectedId}
64 | estimatedItemSize={100}
65 | />
66 |
67 |
68 | );
69 | };
70 |
71 | export default App;
72 |
73 | const styles = StyleSheet.create({
74 | container: {
75 | flex: 1,
76 | marginTop: StatusBar.currentHeight || 0,
77 | },
78 | item: {
79 | padding: 20,
80 | marginVertical: 8,
81 | marginHorizontal: 16,
82 | },
83 | title: {
84 | fontSize: 32,
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/example/app/filter-elements/filter-data-provider.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { createContext, useCallback, useContext, useMemo, useState } from "react";
3 |
4 | export interface CardItem {
5 | id: string;
6 | isExpanded: boolean;
7 | }
8 |
9 | const DataContext = createContext({
10 | data: [] as CardItem[],
11 | setExpanded: (id: string, expanded: boolean) => {},
12 | filter: "",
13 | setFilter: null as unknown as React.Dispatch>,
14 | });
15 |
16 | export const CardsDataProvider = ({
17 | initialData,
18 | children,
19 | }: { initialData: CardItem[]; children: React.ReactNode }) => {
20 | const [data, setData] = useState(initialData);
21 | const [filter, setFilter] = useState("");
22 |
23 | const setExpanded = useCallback((id: string, expanded: boolean) => {
24 | setData((prevData) => {
25 | return prevData.map((item) => {
26 | if (item.id === id) {
27 | return { ...item, isExpanded: expanded };
28 | }
29 | return item;
30 | });
31 | });
32 | }, []);
33 |
34 | const filteredData = useMemo(() => {
35 | if (filter !== "") {
36 | const filterLower = filter.toLowerCase();
37 | return data.filter((item) => item.id.includes(filterLower));
38 | }
39 | return data;
40 | }, [filter, data]);
41 |
42 | return (
43 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | export const useCardData = () => {
50 | const contextValue = useContext(DataContext);
51 | if (!contextValue) {
52 | throw new Error("useData must be used within a CardDataProvider");
53 | }
54 | return contextValue;
55 | };
56 |
--------------------------------------------------------------------------------
/example/app/filter-elements/index.tsx:
--------------------------------------------------------------------------------
1 | import { renderItem } from "@/app/cards-renderItem";
2 | import { DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3 | import { LegendList, type LegendListRef } from "@legendapp/list";
4 | import { useNavigation } from "expo-router";
5 | import { useEffect, useRef, useState } from "react";
6 | import { Button, StyleSheet, Text, TextInput, View } from "react-native";
7 | import { CardsDataProvider, useCardData } from "./filter-data-provider";
8 |
9 | interface CardsProps {
10 | numColumns?: number;
11 | }
12 |
13 | function FilteredCards({ numColumns = 1 }: CardsProps) {
14 | const listRef = useRef(null);
15 | const { data } = useCardData();
16 | const navigation = useNavigation();
17 | const [mvcp, setMvcp] = useState(false);
18 | const [key, setKey] = useState(0);
19 |
20 | useEffect(() => {
21 | navigation.setOptions({
22 | title: "Filter",
23 | headerRight: () => (
24 |