@expo/ui 58.0.5 → 58.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +9 -0
  4. package/android/src/main/java/expo/modules/ui/LazyColumnView.kt +6 -1
  5. package/android/src/main/java/expo/modules/ui/LazyItemsView.kt +313 -0
  6. package/android/src/main/java/expo/modules/ui/LazyRowView.kt +6 -1
  7. package/android/src/main/java/expo/modules/ui/RNHostView.kt +18 -4
  8. package/build/jetpack-compose/LazyColumn/index.d.ts +6 -0
  9. package/build/jetpack-compose/LazyColumn/index.js +2 -0
  10. package/build/jetpack-compose/LazyColumn/index.js.map +1 -1
  11. package/build/jetpack-compose/LazyItems/index.d.ts +54 -0
  12. package/build/jetpack-compose/LazyItems/index.js +63 -0
  13. package/build/jetpack-compose/LazyItems/index.js.map +1 -0
  14. package/build/jetpack-compose/LazyRow/index.d.ts +6 -0
  15. package/build/jetpack-compose/LazyRow/index.js +2 -0
  16. package/build/jetpack-compose/LazyRow/index.js.map +1 -1
  17. package/build/jetpack-compose/index.d.ts +1 -0
  18. package/build/jetpack-compose/index.js.map +1 -1
  19. package/build/recycling/useRecycledRows.d.ts +52 -0
  20. package/build/recycling/useRecycledRows.js +121 -0
  21. package/build/recycling/useRecycledRows.js.map +1 -0
  22. package/build/recycling/window.d.ts +5 -0
  23. package/build/recycling/window.js +28 -0
  24. package/build/recycling/window.js.map +1 -0
  25. package/build/swift-ui/List/DataListForEach.d.ts +52 -0
  26. package/build/swift-ui/List/DataListForEach.js +46 -0
  27. package/build/swift-ui/List/DataListForEach.js.map +1 -0
  28. package/build/swift-ui/List/ListForEach.d.ts +15 -4
  29. package/build/swift-ui/List/ListForEach.js +41 -3
  30. package/build/swift-ui/List/ListForEach.js.map +1 -1
  31. package/build/swift-ui/List/index.d.ts +1 -1
  32. package/build/swift-ui/List/index.js.map +1 -1
  33. package/build/swift-ui/modifiers/index.d.ts +16 -7
  34. package/build/swift-ui/modifiers/index.js +10 -5
  35. package/build/swift-ui/modifiers/index.js.map +1 -1
  36. package/build/swift-ui/modifiers/presentationModifiers.d.ts +8 -0
  37. package/build/swift-ui/modifiers/presentationModifiers.js +9 -0
  38. package/build/swift-ui/modifiers/presentationModifiers.js.map +1 -1
  39. package/build/universal/transformStyle.ios.js +4 -3
  40. package/build/universal/transformStyle.ios.js.map +1 -1
  41. package/expo-module.config.json +1 -1
  42. package/ios/DataListForEachView.swift +255 -0
  43. package/ios/ExpoUIModule.swift +3 -0
  44. package/ios/ListForEachView.swift +12 -3
  45. package/ios/Modifiers/PresentationModifiers.swift +10 -0
  46. package/ios/Modifiers/ViewModifierRegistry.swift +4 -0
  47. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar → 58.0.7/expo.modules.ui-58.0.7-sources.jar} +0 -0
  48. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7-sources.jar.md5 +1 -0
  49. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7-sources.jar.sha1 +1 -0
  50. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7-sources.jar.sha256 +1 -0
  51. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7-sources.jar.sha512 +1 -0
  52. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.aar +0 -0
  53. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.aar.md5 +1 -0
  54. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.aar.sha1 +1 -0
  55. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.aar.sha256 +1 -0
  56. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.aar.sha512 +1 -0
  57. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.module → 58.0.7/expo.modules.ui-58.0.7.module} +23 -23
  58. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.module.md5 +1 -0
  59. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.module.sha1 +1 -0
  60. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.module.sha256 +1 -0
  61. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.module.sha512 +1 -0
  62. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.pom → 58.0.7/expo.modules.ui-58.0.7.pom} +2 -2
  63. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.pom.md5 +1 -0
  64. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.pom.sha1 +1 -0
  65. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.pom.sha256 +1 -0
  66. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.7/expo.modules.ui-58.0.7.pom.sha512 +1 -0
  67. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  68. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  69. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  70. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  71. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  72. package/package.json +4 -4
  73. package/prebuilds/output/debug/xcframeworks/ExpoUI.tar.gz +0 -0
  74. package/prebuilds/output/release/xcframeworks/ExpoUI.tar.gz +0 -0
  75. package/src/jetpack-compose/LazyColumn/index.tsx +3 -0
  76. package/src/jetpack-compose/LazyItems/index.tsx +121 -0
  77. package/src/jetpack-compose/LazyRow/index.tsx +3 -0
  78. package/src/jetpack-compose/index.ts +1 -0
  79. package/src/recycling/useRecycledRows.tsx +200 -0
  80. package/src/recycling/window.ts +28 -0
  81. package/src/swift-ui/List/DataListForEach.tsx +128 -0
  82. package/src/swift-ui/List/ListForEach.tsx +57 -6
  83. package/src/swift-ui/List/index.tsx +1 -1
  84. package/src/swift-ui/modifiers/index.ts +50 -14
  85. package/src/swift-ui/modifiers/presentationModifiers.ts +10 -0
  86. package/src/universal/transformStyle.ios.ts +5 -2
  87. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5-sources.jar.md5 +0 -1
  88. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5-sources.jar.sha1 +0 -1
  89. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5-sources.jar.sha256 +0 -1
  90. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5-sources.jar.sha512 +0 -1
  91. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.aar +0 -0
  92. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.aar.md5 +0 -1
  93. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.aar.sha1 +0 -1
  94. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.aar.sha256 +0 -1
  95. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.aar.sha512 +0 -1
  96. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.md5 +0 -1
  97. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha1 +0 -1
  98. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha256 +0 -1
  99. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha512 +0 -1
  100. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.md5 +0 -1
  101. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha1 +0 -1
  102. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha256 +0 -1
  103. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha512 +0 -1
@@ -0,0 +1,121 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { Fragment, type ReactElement } from 'react';
3
+
4
+ import {
5
+ useItemKeys,
6
+ useRecycledRows,
7
+ type RecycledSlotProps,
8
+ type RenderItem,
9
+ type WindowChangeEvent,
10
+ } from '../../recycling/useRecycledRows';
11
+ import { type ViewEvent } from '../../types';
12
+
13
+ export interface LazyItemsProps<ItemT> {
14
+ /** Items to display. Replace the array when updating data. */
15
+ data: readonly ItemT[];
16
+ /** Returns a stable, unique string key, also used as the lazy list item key. */
17
+ keyExtractor: (item: ItemT, index: number) => string;
18
+ /**
19
+ * Renders a row. Wrap it in `useCallback`, or every row re-renders on each parent render.
20
+ * Recycled rows are reused for other items, so their local state (`useState`) carries over.
21
+ * Reset it when the item changes, or keep the state outside the row.
22
+ */
23
+ children: RenderItem<ItemT>;
24
+ /**
25
+ * Renders only the rows near the visible range and reuses them while scrolling. Set to `false` to
26
+ * render every row at once. Set it once; changing it remounts the rows.
27
+ * @default true
28
+ */
29
+ recycling?: boolean;
30
+ /**
31
+ * Extra rows to prepare on each side of the visible rows. Must be a non-negative integer.
32
+ * Ignored when `recycling` is `false`.
33
+ * @default 10
34
+ */
35
+ overscanCount?: number;
36
+ /**
37
+ * Placeholder size in dp along the scroll axis, until a row is measured. Must be positive.
38
+ * Measurements reset when the data changes. In `LazyColumn.Items` they also reset when the list
39
+ * width changes. Ignored when `recycling` is `false`.
40
+ * @default 64
41
+ */
42
+ estimatedItemSize?: number;
43
+ }
44
+
45
+ type NativeLazyItemsProps = ViewEvent<'onWindowChange', WindowChangeEvent> & {
46
+ itemKeys: string[];
47
+ revision: number;
48
+ estimatedItemSize: number;
49
+ children: ReactElement;
50
+ };
51
+
52
+ const LazyItemsNativeView: React.ComponentType<NativeLazyItemsProps> =
53
+ requireNativeView<NativeLazyItemsProps>('ExpoUI', 'LazyItemsView');
54
+ const LazyItemsSlotNativeView: React.ComponentType<RecycledSlotProps> =
55
+ requireNativeView<RecycledSlotProps>('ExpoUI', 'LazyItemsSlotView');
56
+ const LazyItemsPoolNativeView: React.ComponentType<{ children: ReactElement[] }> =
57
+ requireNativeView('ExpoUI', 'LazyItemsPoolView');
58
+
59
+ /**
60
+ * A block of recycled rows inside `LazyColumn` or `LazyRow`, mirroring the Compose
61
+ * `items(count, key)` builder. Only a small pool of rows around the visible range is mounted,
62
+ * so large data sets stay cheap. Rows that are not ready yet show a placeholder of
63
+ * `estimatedItemSize`, or of the size last measured for that item.
64
+ *
65
+ * Mount it as a direct child of `LazyColumn` or `LazyRow`. Plain children of the same list stay
66
+ * single items, so static and recycled content can be mixed in order.
67
+ * @platform android
68
+ */
69
+ export const LazyItems = createLazyItems('LazyColumn.Items');
70
+
71
+ /**
72
+ * @hidden
73
+ * Creates the `Items` component of one lazy list, so its errors name that list.
74
+ */
75
+ export function createLazyItems(componentName: string) {
76
+ function Items<ItemT>({ recycling = true, ...props }: LazyItemsProps<ItemT>) {
77
+ return recycling ? <RecycledItems {...props} /> : <StaticItems {...props} />;
78
+ }
79
+
80
+ // Each row is a plain child of the lazy list, so it becomes one lazy item.
81
+ function StaticItems<ItemT>({
82
+ data,
83
+ keyExtractor,
84
+ children: renderItem,
85
+ }: Omit<LazyItemsProps<ItemT>, 'recycling'>) {
86
+ const itemKeys = useItemKeys(componentName, data, keyExtractor);
87
+ return data.map((item, index) => (
88
+ <Fragment key={itemKeys[index]}>{renderItem({ item, index })}</Fragment>
89
+ ));
90
+ }
91
+
92
+ function RecycledItems<ItemT>({
93
+ data,
94
+ keyExtractor,
95
+ children: renderItem,
96
+ overscanCount,
97
+ estimatedItemSize = 64,
98
+ }: Omit<LazyItemsProps<ItemT>, 'recycling'>) {
99
+ const { itemKeys, revision, rows, onWindowChange } = useRecycledRows({
100
+ componentName,
101
+ Slot: LazyItemsSlotNativeView,
102
+ data,
103
+ keyExtractor,
104
+ renderItem,
105
+ overscanCount,
106
+ estimatedItemSize,
107
+ });
108
+
109
+ return (
110
+ <LazyItemsNativeView
111
+ itemKeys={itemKeys}
112
+ revision={revision}
113
+ estimatedItemSize={estimatedItemSize}
114
+ onWindowChange={({ nativeEvent }) => onWindowChange(nativeEvent)}>
115
+ <LazyItemsPoolNativeView>{rows}</LazyItemsPoolNativeView>
116
+ </LazyItemsNativeView>
117
+ );
118
+ }
119
+ Items.displayName = componentName;
120
+ return Items;
121
+ }
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
2
2
 
3
3
  import type { ExpoModifier } from '../../types';
4
4
  import { type ContentPadding } from '../LazyColumn';
5
+ import { createLazyItems } from '../LazyItems';
5
6
  import { createViewModifierEventListener } from '../modifiers/utils';
6
7
 
7
8
  export interface LazyRowProps {
@@ -56,3 +57,5 @@ function transformProps(props: LazyRowProps): NativeLazyRowProps {
56
57
  export function LazyRow(props: LazyRowProps) {
57
58
  return <LazyRowNativeView {...transformProps(props)} />;
58
59
  }
60
+
61
+ LazyRow.Items = createLazyItems('LazyRow.Items');
@@ -17,6 +17,7 @@ export * from './ExposedDropdownMenuBox';
17
17
  export * from './Divider';
18
18
  export * from './Host';
19
19
  export * from './LazyColumn';
20
+ export { type LazyItemsProps } from './LazyItems';
20
21
  export * from './LazyRow';
21
22
  export * from './ListItem';
22
23
  export * from './RNHostView';
@@ -0,0 +1,200 @@
1
+ import {
2
+ memo,
3
+ useLayoutEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ComponentType,
8
+ type ReactElement,
9
+ } from 'react';
10
+
11
+ import { getSlotIndices, getWindow } from './window';
12
+
13
+ /** Payload of the native event asking for the rows around the visible range. */
14
+ export type WindowChangeEvent = { first: number; last: number; revision: number };
15
+
16
+ /** Props of the native view that holds one recycled row. */
17
+ export type RecycledSlotProps = {
18
+ itemKey: string;
19
+ index: number;
20
+ revision: number;
21
+ children: ReactElement;
22
+ };
23
+
24
+ /** Renders one row of the data set. */
25
+ export type RenderItem<ItemT> = (info: { item: ItemT; index: number }) => ReactElement;
26
+
27
+ type RecycledRowProps<ItemT> = Omit<RecycledSlotProps, 'children'> & {
28
+ Slot: ComponentType<RecycledSlotProps>;
29
+ item: ItemT;
30
+ renderItem: RenderItem<ItemT>;
31
+ };
32
+
33
+ // Skip unchanged rows when the window moves.
34
+ const RecycledRow = memo(function RecycledRow<ItemT>({
35
+ Slot,
36
+ item,
37
+ index,
38
+ itemKey,
39
+ revision,
40
+ renderItem,
41
+ }: RecycledRowProps<ItemT>) {
42
+ return (
43
+ <Slot itemKey={itemKey} index={index} revision={revision}>
44
+ {renderItem({ item, index })}
45
+ </Slot>
46
+ );
47
+ }) as <ItemT>(props: RecycledRowProps<ItemT>) => ReactElement;
48
+
49
+ export type RecycledRows = {
50
+ /** Keys of every item, so the native list can size and identify the full data set. */
51
+ itemKeys: string[];
52
+ /** Increases whenever the data changes, so late native events can be dropped. */
53
+ revision: number;
54
+ /** The pooled rows to mount, one per slot. */
55
+ rows: ReactElement[];
56
+ /** Applies a native window request. */
57
+ onWindowChange: (event: WindowChangeEvent) => void;
58
+ /** Whether a native event was sent for the data currently committed to native. */
59
+ isCurrentRevision: (revision: number) => boolean;
60
+ };
61
+
62
+ /**
63
+ * Returns one key per item. Keys follow `data` only: an inline `keyExtractor` changes identity on
64
+ * every parent render, and recomputing keys for it would bump the revision and re-render every row.
65
+ */
66
+ export function useItemKeys<ItemT>(
67
+ componentName: string,
68
+ data: readonly ItemT[],
69
+ keyExtractor: (item: ItemT, index: number) => string
70
+ ): string[] {
71
+ const keyExtractorRef = useRef(keyExtractor);
72
+ keyExtractorRef.current = keyExtractor;
73
+ return useMemo(() => {
74
+ const keys = data.map((item, index) => keyExtractorRef.current(item, index));
75
+ if (keys.some((key) => typeof key !== 'string') || new Set(keys).size !== keys.length) {
76
+ throw new Error(`${componentName} keyExtractor must return a unique string for every item.`);
77
+ }
78
+ return keys;
79
+ }, [data]);
80
+ }
81
+
82
+ /**
83
+ * Keeps a fixed pool of rows centered on the range the native list asks for.
84
+ * Item `index` always renders in slot `index % capacity`, so moving the window
85
+ * only re-renders the slots whose item changed.
86
+ */
87
+ export function useRecycledRows<ItemT>({
88
+ componentName,
89
+ Slot,
90
+ data,
91
+ keyExtractor,
92
+ renderItem,
93
+ overscanCount = 10,
94
+ estimatedItemSize = 64,
95
+ }: {
96
+ /** Name used in error messages, for example `List.ForEach`. */
97
+ componentName: string;
98
+ /** Native view that holds one recycled row. */
99
+ Slot: ComponentType<RecycledSlotProps>;
100
+ data: readonly ItemT[];
101
+ keyExtractor: (item: ItemT, index: number) => string;
102
+ renderItem: RenderItem<ItemT>;
103
+ overscanCount?: number;
104
+ estimatedItemSize?: number;
105
+ }): RecycledRows {
106
+ if (!Number.isSafeInteger(overscanCount) || overscanCount < 0) {
107
+ throw new Error(`${componentName} overscanCount must be a non-negative integer.`);
108
+ }
109
+ if (!Number.isFinite(estimatedItemSize) || estimatedItemSize <= 0) {
110
+ throw new Error(`${componentName} estimatedItemSize must be a positive finite number.`);
111
+ }
112
+ const itemKeys = useItemKeys(componentName, data, keyExtractor);
113
+ const [state, setState] = useState({
114
+ keys: itemKeys,
115
+ first: 0,
116
+ last: 0,
117
+ revision: 0,
118
+ capacity: 0,
119
+ overscanCount,
120
+ });
121
+ let current = state;
122
+ if (state.keys !== itemKeys) {
123
+ const previousKey = state.keys[state.first];
124
+ const anchor = previousKey === undefined ? 0 : Math.max(0, itemKeys.indexOf(previousKey));
125
+ current = {
126
+ ...state,
127
+ keys: itemKeys,
128
+ first: Math.min(anchor, Math.max(0, data.length - 1)),
129
+ last: Math.min(anchor + state.last - state.first, Math.max(0, data.length - 1)),
130
+ revision: state.revision + 1,
131
+ };
132
+ }
133
+ if (current.overscanCount !== overscanCount) {
134
+ current = { ...current, overscanCount, capacity: 0 };
135
+ }
136
+ if (current !== state) setState(current);
137
+ const { start, capacity } = getWindow(
138
+ data.length,
139
+ current.first,
140
+ current.last,
141
+ overscanCount,
142
+ current.capacity
143
+ );
144
+ const committedRevision = useRef(current.revision);
145
+ useLayoutEffect(() => {
146
+ committedRevision.current = current.revision;
147
+ }, [current.revision]);
148
+
149
+ const rows = getSlotIndices(start, capacity).map((index, slot) => {
150
+ const item = data[index];
151
+ const itemKey = itemKeys[index];
152
+ if (item === undefined || itemKey === undefined) {
153
+ throw new Error(`${componentName} could not resolve the item at index ${index}.`);
154
+ }
155
+ return (
156
+ <RecycledRow
157
+ key={slot}
158
+ Slot={Slot}
159
+ item={item}
160
+ index={index}
161
+ itemKey={itemKey}
162
+ revision={current.revision}
163
+ renderItem={renderItem}
164
+ />
165
+ );
166
+ });
167
+
168
+ return {
169
+ itemKeys,
170
+ revision: current.revision,
171
+ rows,
172
+ isCurrentRevision: (revision) => revision === committedRevision.current,
173
+ onWindowChange: (event) => {
174
+ if (
175
+ !Number.isSafeInteger(event.first) ||
176
+ !Number.isSafeInteger(event.last) ||
177
+ event.last < event.first
178
+ ) {
179
+ return;
180
+ }
181
+ setState((previous) => {
182
+ // Ignore requests from before a data change.
183
+ if (event.revision !== previous.revision) return previous;
184
+ const maximum = Math.max(0, previous.keys.length - 1);
185
+ const first = Math.max(0, Math.min(event.first, maximum));
186
+ const last = Math.max(first, Math.min(event.last, maximum));
187
+ const { capacity } = getWindow(
188
+ previous.keys.length,
189
+ first,
190
+ last,
191
+ previous.overscanCount,
192
+ previous.capacity
193
+ );
194
+ return first === previous.first && last === previous.last && capacity === previous.capacity
195
+ ? previous
196
+ : { ...previous, first, last, capacity };
197
+ });
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,28 @@
1
+ export function getWindow(
2
+ count: number,
3
+ first: number,
4
+ last: number,
5
+ overscanCount: number,
6
+ minCapacity = 0
7
+ ) {
8
+ if (count === 0) return { start: 0, capacity: 0 };
9
+ first = Math.max(0, Math.min(first, count - 1));
10
+ last = Math.max(first, Math.min(last, count - 1));
11
+ const start = Math.max(0, first - overscanCount);
12
+ const end = Math.min(count, last + 1 + overscanCount);
13
+ // Never shrink the pool: a stable capacity keeps `index % capacity` slot assignments stable.
14
+ // Grow it with headroom, because every growth re-renders the whole pool.
15
+ const needed = end - start;
16
+ let capacity = minCapacity;
17
+ if (needed > capacity) capacity = capacity === 0 ? needed : needed + overscanCount;
18
+ capacity = Math.min(count, capacity);
19
+ return { start: Math.min(start, count - capacity), capacity };
20
+ }
21
+
22
+ export function getSlotIndices(start: number, capacity: number): number[] {
23
+ if (capacity === 0) return [];
24
+ return Array.from(
25
+ { length: capacity },
26
+ (_, slot) => start + ((slot - (start % capacity) + capacity) % capacity)
27
+ );
28
+ }
@@ -0,0 +1,128 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { type ReactElement } from 'react';
3
+
4
+ import {
5
+ useRecycledRows,
6
+ type RecycledSlotProps,
7
+ type WindowChangeEvent,
8
+ } from '../../recycling/useRecycledRows';
9
+ import { type ViewEvent } from '../../types';
10
+ import { createViewModifierEventListener } from '../modifiers/utils';
11
+ import { type CommonViewModifierProps } from '../types';
12
+
13
+ export interface ListForEachProps<ItemT> extends CommonViewModifierProps {
14
+ /** Called with deleted indices from this group's `data` array. */
15
+ onDelete?: (indices: number[]) => void;
16
+ /**
17
+ * Called with the source indices and the destination index. The destination index counts
18
+ * positions before the moved items are removed, so it can equal `data.length`.
19
+ */
20
+ onMove?: (sourceIndices: number[], destination: number) => void;
21
+ /** Items to display. Replace the array when updating data. */
22
+ data: readonly ItemT[];
23
+ /** Returns a stable, unique string key, also used for `List` selection. */
24
+ keyExtractor: (item: ItemT, index: number) => string;
25
+ /**
26
+ * Renders a row. When `recycling` is `true`, wrap it in `useCallback`, or every row
27
+ * re-renders on each parent render.
28
+ * Recycled rows are reused for other items, so their local state (`useState`) carries over.
29
+ * Reset it when the item changes, or keep the state outside the row.
30
+ */
31
+ children: (info: { item: ItemT; index: number }) => ReactElement;
32
+ /**
33
+ * Renders only the rows near the visible range and reuses them while scrolling. Set to `false` to
34
+ * render every row at once. Set it once; changing it remounts the rows.
35
+ * @default true
36
+ */
37
+ recycling?: boolean;
38
+ /**
39
+ * Extra rows to prepare on each side of the visible rows. Must be a non-negative integer.
40
+ * Ignored when `recycling` is `false`.
41
+ * @default 10
42
+ */
43
+ overscanCount?: number;
44
+ /**
45
+ * Placeholder height in points, excluding `List` insets, until a row is measured.
46
+ * Must be positive. Measurements reset when `data` or width changes.
47
+ * Ignored when `recycling` is `false`.
48
+ * @default 64
49
+ */
50
+ estimatedItemSize?: number;
51
+ }
52
+
53
+ type NativeProps = CommonViewModifierProps &
54
+ ViewEvent<'onWindowChange', WindowChangeEvent> &
55
+ ViewEvent<'onDelete', { indices: number[]; revision: number }> &
56
+ ViewEvent<'onMove', { sourceIndices: number[]; destination: number; revision: number }> & {
57
+ deleteEnabled: boolean;
58
+ moveEnabled: boolean;
59
+ itemKeys: string[];
60
+ revision: number;
61
+ estimatedItemSize: number;
62
+ children: ReactElement;
63
+ };
64
+
65
+ const NativeList = requireNativeView<NativeProps>('ExpoUI', 'DataListForEachView');
66
+ export const NativeSlot = requireNativeView<RecycledSlotProps>('ExpoUI', 'DataListForEachItemView');
67
+ const NativePool = requireNativeView<{ children: ReactElement[] }>(
68
+ 'ExpoUI',
69
+ 'DataListForEachPoolView'
70
+ );
71
+
72
+ /**
73
+ * Renders a window of recycled rows inside List or Section.
74
+ * Rows show placeholders while JS prepares content. Native keys still cover the full data set.
75
+ * @platform ios
76
+ */
77
+ export function DataListForEach<ItemT>({
78
+ data,
79
+ keyExtractor,
80
+ children: renderItem,
81
+ overscanCount,
82
+ estimatedItemSize = 64,
83
+ modifiers,
84
+ onDelete,
85
+ onMove,
86
+ ...props
87
+ }: ListForEachProps<ItemT>) {
88
+ const { itemKeys, revision, rows, onWindowChange, isCurrentRevision } = useRecycledRows({
89
+ componentName: 'List.ForEach',
90
+ Slot: NativeSlot,
91
+ data,
92
+ keyExtractor,
93
+ renderItem,
94
+ overscanCount,
95
+ estimatedItemSize,
96
+ });
97
+
98
+ return (
99
+ <NativeList
100
+ {...props}
101
+ modifiers={modifiers}
102
+ {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
103
+ deleteEnabled={!!onDelete}
104
+ moveEnabled={!!onMove}
105
+ onDelete={
106
+ onDelete
107
+ ? ({ nativeEvent }) => {
108
+ if (isCurrentRevision(nativeEvent.revision)) onDelete(nativeEvent.indices);
109
+ }
110
+ : undefined
111
+ }
112
+ onMove={
113
+ onMove
114
+ ? ({ nativeEvent }) => {
115
+ if (isCurrentRevision(nativeEvent.revision)) {
116
+ onMove(nativeEvent.sourceIndices, nativeEvent.destination);
117
+ }
118
+ }
119
+ : undefined
120
+ }
121
+ itemKeys={itemKeys}
122
+ revision={revision}
123
+ estimatedItemSize={estimatedItemSize}
124
+ onWindowChange={({ nativeEvent }) => onWindowChange(nativeEvent)}>
125
+ <NativePool>{rows}</NativePool>
126
+ </NativeList>
127
+ );
128
+ }
@@ -1,7 +1,11 @@
1
1
  import { requireNativeView } from 'expo';
2
2
 
3
+ import { useItemKeys } from '../../recycling/useRecycledRows';
3
4
  import { type ViewEvent } from '../../types';
4
5
  import { type CommonViewModifierProps } from '../types';
6
+ import { DataListForEach, NativeSlot, type ListForEachProps } from './DataListForEach';
7
+
8
+ export { type ListForEachProps };
5
9
 
6
10
  const ListForEachNativeView: React.ComponentType<NativeListForEachProps> =
7
11
  requireNativeView<NativeListForEachProps>('ExpoUI', 'ListForEachView');
@@ -14,21 +18,29 @@ type NativeListForEachProps = CommonViewModifierProps &
14
18
  DeleteEvent &
15
19
  MoveEvent & {
16
20
  children: React.ReactNode;
21
+ deleteEnabled: boolean;
22
+ moveEnabled: boolean;
17
23
  };
18
24
 
19
- export interface ListForEachProps extends CommonViewModifierProps {
25
+ /**
26
+ * @deprecated Pass `data` and `keyExtractor`, and render each row from a `children` function.
27
+ */
28
+ export interface ListForEachElementsProps extends CommonViewModifierProps {
29
+ data?: never;
30
+ keyExtractor?: never;
31
+ recycling?: never;
32
+ overscanCount?: never;
33
+ estimatedItemSize?: never;
20
34
  /**
21
35
  * The children elements to be rendered inside the `List.ForEach`.
22
36
  */
23
37
  children: React.ReactNode;
24
-
25
38
  /**
26
39
  * Callback triggered when items are deleted.
27
40
  * Receives an array of indices that were deleted.
28
41
  * @see Official [SwiftUI documentation](https://developer.apple.com/documentation/swiftui/dynamicviewcontent/ondelete(perform:)).
29
42
  */
30
43
  onDelete?: (indices: number[]) => void;
31
-
32
44
  /**
33
45
  * Callback triggered when items are moved.
34
46
  * Receives the source indices and destination index.
@@ -37,14 +49,33 @@ export interface ListForEachProps extends CommonViewModifierProps {
37
49
  onMove?: (sourceIndices: number[], destination: number) => void;
38
50
  }
39
51
 
52
+ let warnedElements = false;
53
+
40
54
  /**
41
- * A compound component of `List` that enables item deletion and reordering.
42
- * This component must be used as a child of `List` (as `List.ForEach`).
55
+ * A group of rows inside `List`, with optional deletion and reordering.
56
+ * Pass `data` with `keyExtractor`, and render each row from a `children` function.
57
+ * Recycles rows unless `recycling` is `false`.
43
58
  */
44
- export function ListForEach({ children, onDelete, onMove, ...props }: ListForEachProps) {
59
+ export function ListForEach<ItemT>(props: ListForEachProps<ItemT> | ListForEachElementsProps) {
60
+ if (props.data === undefined) {
61
+ if (__DEV__ && !warnedElements) {
62
+ warnedElements = true;
63
+ console.warn(
64
+ '[@expo/ui] Passing elements as List.ForEach children is deprecated. Pass `data` and `keyExtractor`, and render each row from a `children` function: `{({ item }) => <Row item={item} />}`.'
65
+ );
66
+ }
67
+ return <ChildrenListForEach {...props} />;
68
+ }
69
+ const { recycling = true, ...rest } = props;
70
+ return recycling ? <DataListForEach {...rest} /> : <StaticListForEach {...rest} />;
71
+ }
72
+
73
+ function ChildrenListForEach({ children, onDelete, onMove, ...props }: ListForEachElementsProps) {
45
74
  return (
46
75
  <ListForEachNativeView
47
76
  {...props}
77
+ deleteEnabled={!!onDelete}
78
+ moveEnabled={!!onMove}
48
79
  onDelete={onDelete ? ({ nativeEvent }) => onDelete(nativeEvent.indices) : undefined}
49
80
  onMove={
50
81
  onMove
@@ -55,3 +86,23 @@ export function ListForEach({ children, onDelete, onMove, ...props }: ListForEac
55
86
  </ListForEachNativeView>
56
87
  );
57
88
  }
89
+
90
+ function StaticListForEach<ItemT>({
91
+ data,
92
+ keyExtractor,
93
+ children: renderItem,
94
+ overscanCount: _overscanCount,
95
+ estimatedItemSize: _estimatedItemSize,
96
+ ...props
97
+ }: Omit<ListForEachProps<ItemT>, 'recycling'>) {
98
+ const itemKeys = useItemKeys('List.ForEach', data, keyExtractor);
99
+ return (
100
+ <ChildrenListForEach {...props}>
101
+ {data.map((item, index) => (
102
+ <NativeSlot key={itemKeys[index]} itemKey={itemKeys[index]!} index={index} revision={0}>
103
+ {renderItem({ item, index })}
104
+ </NativeSlot>
105
+ ))}
106
+ </ChildrenListForEach>
107
+ );
108
+ }
@@ -5,7 +5,7 @@ import { createViewModifierEventListener } from '../modifiers/utils';
5
5
  import { type CommonViewModifierProps } from '../types';
6
6
  import { ListForEach } from './ListForEach';
7
7
 
8
- export { ListForEach, type ListForEachProps } from './ListForEach';
8
+ export { ListForEach, type ListForEachProps, type ListForEachElementsProps } from './ListForEach';
9
9
 
10
10
  const ListNativeView: React.ComponentType<NativeListProps> = requireNativeView<NativeListProps>(
11
11
  'ExpoUI',