├── .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 |