@shopify/flash-list 2.0.0-alpha.9 → 2.0.0-rc.2

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 (180) hide show
  1. package/README.md +37 -97
  2. package/dist/AnimatedFlashList.d.ts.map +1 -1
  3. package/dist/AnimatedFlashList.js +3 -3
  4. package/dist/AnimatedFlashList.js.map +1 -1
  5. package/dist/FlashList.d.ts +9 -0
  6. package/dist/FlashList.d.ts.map +1 -1
  7. package/dist/FlashList.js +20 -0
  8. package/dist/FlashList.js.map +1 -1
  9. package/dist/FlashListProps.d.ts +15 -8
  10. package/dist/FlashListProps.d.ts.map +1 -1
  11. package/dist/FlashListProps.js.map +1 -1
  12. package/dist/FlashListRef.d.ts +305 -0
  13. package/dist/FlashListRef.d.ts.map +1 -0
  14. package/dist/FlashListRef.js +3 -0
  15. package/dist/FlashListRef.js.map +1 -0
  16. package/dist/MasonryFlashList.js.map +1 -1
  17. package/dist/__tests__/RecyclerView.test.js +62 -27
  18. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  19. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  20. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  21. package/dist/__tests__/RenderStackManager.test.js +486 -0
  22. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  23. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  24. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  25. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  26. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  28. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  29. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  34. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  35. package/dist/native/config/PlatformHelper.android.js +1 -0
  36. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  40. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  41. package/dist/native/config/PlatformHelper.ios.js +1 -0
  42. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  43. package/dist/native/config/PlatformHelper.js +1 -0
  44. package/dist/native/config/PlatformHelper.js.map +1 -1
  45. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  46. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  47. package/dist/native/config/PlatformHelper.web.js +1 -0
  48. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  49. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  50. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  51. package/dist/recyclerview/RecyclerView.js +63 -45
  52. package/dist/recyclerview/RecyclerView.js.map +1 -1
  53. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  54. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  56. package/dist/recyclerview/RecyclerViewManager.d.ts +21 -7
  57. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  58. package/dist/recyclerview/RecyclerViewManager.js +105 -113
  59. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  60. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  61. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  62. package/dist/recyclerview/RenderStackManager.js +324 -0
  63. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  64. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  65. package/dist/recyclerview/ViewHolder.js +5 -3
  66. package/dist/recyclerview/ViewHolder.js.map +1 -1
  67. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  68. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  69. package/dist/recyclerview/ViewHolderCollection.js +23 -8
  70. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  71. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  72. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  73. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  74. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  75. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  76. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  77. package/dist/recyclerview/components/StickyHeaders.js +40 -33
  78. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  79. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  80. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  81. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  82. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  83. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
  84. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  85. package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
  86. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  87. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  88. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  89. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  90. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  91. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  92. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  93. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  94. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  95. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  96. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  97. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  98. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  99. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  100. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  101. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  102. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  103. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  104. package/dist/recyclerview/hooks/useRecyclerViewController.js +174 -123
  105. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  106. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  107. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  108. package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
  109. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  110. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  111. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  112. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  114. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  115. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  116. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  117. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  118. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  119. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +10 -16
  120. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  121. package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
  122. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  123. package/dist/tsconfig.tsbuildinfo +1 -1
  124. package/dist/viewability/ViewToken.d.ts +2 -2
  125. package/dist/viewability/ViewToken.d.ts.map +1 -1
  126. package/dist/viewability/ViewabilityHelper.js +1 -1
  127. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  128. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  129. package/dist/viewability/ViewabilityManager.js +1 -2
  130. package/dist/viewability/ViewabilityManager.js.map +1 -1
  131. package/jestSetup.js +30 -11
  132. package/package.json +2 -1
  133. package/src/AnimatedFlashList.ts +3 -2
  134. package/src/FlashList.tsx +24 -0
  135. package/src/FlashListProps.ts +20 -8
  136. package/src/FlashListRef.ts +320 -0
  137. package/src/MasonryFlashList.tsx +2 -2
  138. package/src/__tests__/RecyclerView.test.tsx +83 -29
  139. package/src/__tests__/RenderStackManager.test.ts +575 -0
  140. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  141. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  142. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  143. package/src/index.ts +1 -0
  144. package/src/native/config/PlatformHelper.android.ts +1 -0
  145. package/src/native/config/PlatformHelper.ios.ts +1 -0
  146. package/src/native/config/PlatformHelper.ts +1 -0
  147. package/src/native/config/PlatformHelper.web.ts +1 -0
  148. package/src/recyclerview/RecyclerView.tsx +82 -52
  149. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  150. package/src/recyclerview/RecyclerViewManager.ts +123 -98
  151. package/src/recyclerview/RenderStackManager.ts +291 -0
  152. package/src/recyclerview/ViewHolder.tsx +5 -3
  153. package/src/recyclerview/ViewHolderCollection.tsx +33 -12
  154. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  155. package/src/recyclerview/components/StickyHeaders.tsx +63 -45
  156. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  157. package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
  158. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  159. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  160. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  161. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  162. package/src/recyclerview/hooks/useRecyclerViewController.tsx +199 -176
  163. package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
  164. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  165. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  166. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  167. package/src/recyclerview/layout-managers/LayoutManager.ts +12 -21
  168. package/src/viewability/ViewToken.ts +2 -2
  169. package/src/viewability/ViewabilityHelper.ts +1 -1
  170. package/src/viewability/ViewabilityManager.ts +6 -3
  171. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  172. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  173. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  174. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  175. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  176. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  177. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  178. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  179. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  180. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -3,103 +3,88 @@ import ViewabilityManager from "../viewability/ViewabilityManager";
3
3
  import { ConsecutiveNumbers } from "./helpers/ConsecutiveNumbers";
4
4
  import { RVGridLayoutManagerImpl } from "./layout-managers/GridLayoutManager";
5
5
  import {
6
+ LayoutParams,
6
7
  RVDimension,
7
8
  RVLayoutInfo,
8
9
  RVLayoutManager,
10
+ SpanSizeInfo,
9
11
  } from "./layout-managers/LayoutManager";
10
12
  import { RVLinearLayoutManagerImpl } from "./layout-managers/LinearLayoutManager";
11
13
  import { RVMasonryLayoutManagerImpl } from "./layout-managers/MasonryLayoutManager";
12
- import { RecycleKeyManagerImpl, RecycleKeyManager } from "./RecycleKeyManager";
13
14
  import { RecyclerViewProps } from "./RecyclerViewProps";
14
15
  import {
15
16
  RVEngagedIndicesTracker,
16
17
  RVEngagedIndicesTrackerImpl,
17
18
  Velocity,
18
19
  } from "./helpers/EngagedIndicesTracker";
19
-
20
- // Abstracts layout manager, key manager and viewability manager and generates render stack (progressively on load)
20
+ import { RenderStackManager } from "./RenderStackManager";
21
+ // Abstracts layout manager, render stack manager and viewability manager and generates render stack (progressively on load)
21
22
  export class RecyclerViewManager<T> {
22
23
  private initialDrawBatchSize = 1;
23
24
  private engagedIndicesTracker: RVEngagedIndicesTracker;
24
- private recycleKeyManager: RecycleKeyManager;
25
+ private renderStackManager: RenderStackManager;
25
26
  private layoutManager?: RVLayoutManager;
26
27
  // Map of index to key
27
- private renderStack: Map<number, string> = new Map();
28
28
  private isFirstLayoutComplete = false;
29
29
  private hasRenderedProgressively = false;
30
- private props: RecyclerViewProps<T>;
30
+ private propsRef: RecyclerViewProps<T>;
31
31
  private itemViewabilityManager: ViewabilityManager<T>;
32
- private allocatedKeyTracker: Set<string> = new Set();
32
+ private _isDisposed = false;
33
+ private _isLayoutManagerDirty = false;
33
34
 
34
- public disableRecycling = false;
35
35
  public firstItemOffset = 0;
36
36
  public ignoreScrollEvents = false;
37
37
 
38
+ public get isOffsetProjectionEnabled() {
39
+ return this.engagedIndicesTracker.enableOffsetProjection;
40
+ }
41
+
42
+ public get isDisposed() {
43
+ return this._isDisposed;
44
+ }
45
+
38
46
  constructor(props: RecyclerViewProps<T>) {
39
- this.props = props;
47
+ this.getStableId = this.getStableId.bind(this);
48
+ this.getItemType = this.getItemType.bind(this);
49
+ this.overrideItemLayout = this.overrideItemLayout.bind(this);
50
+ this.propsRef = props;
40
51
  this.engagedIndicesTracker = new RVEngagedIndicesTrackerImpl();
41
- this.recycleKeyManager = new RecycleKeyManagerImpl();
52
+ this.renderStackManager = new RenderStackManager(
53
+ props.maxItemsInRecyclePool
54
+ );
42
55
  this.itemViewabilityManager = new ViewabilityManager<T>(this as any);
43
56
  }
44
57
 
45
58
  // updates render stack based on the engaged indices which are sorted. Recycles unused keys.
46
- // TODO: Write comprehensive tests for this function
47
59
  private updateRenderStack = (engagedIndices: ConsecutiveNumbers): void => {
48
- // console.log("updateRenderStack", engagedIndices);
49
-
50
- this.allocatedKeyTracker.clear();
51
- const newRenderStack = new Map<number, string>();
52
- for (const [index, key] of this.renderStack) {
53
- if (!engagedIndices.includes(index)) {
54
- this.recycleKeyManager.recycleKey(key);
55
- }
56
- }
57
- if (this.disableRecycling) {
58
- this.recycleKeyManager.clearPool();
59
- }
60
- for (const index of engagedIndices) {
61
- const currentKey = this.renderStack.get(index);
62
-
63
- if (
64
- currentKey &&
65
- !this.disableRecycling &&
66
- !this.allocatedKeyTracker.has(currentKey)
67
- ) {
68
- this.recycleKeyManager.recycleKey(currentKey);
69
- }
60
+ this.renderStackManager.sync(
61
+ this.getStableId,
62
+ this.getItemType,
63
+ engagedIndices,
64
+ this.getDataLength()
65
+ );
66
+ };
70
67
 
71
- const newKey = this.recycleKeyManager.getKey(
72
- this.getItemType(index),
73
- this.getStableId(index),
74
- currentKey
75
- );
76
- this.allocatedKeyTracker.add(newKey);
77
- newRenderStack.set(index, newKey);
78
- }
79
- // DANGER
80
- for (const [index, key] of this.renderStack) {
81
- if (
82
- this.recycleKeyManager.hasKeyInPool(key) &&
83
- !newRenderStack.has(index) &&
84
- index < (this.props.data?.length ?? 0)
85
- ) {
86
- this.allocatedKeyTracker.add(key);
87
- newRenderStack.set(index, key);
88
- }
89
- }
68
+ get props() {
69
+ return this.propsRef;
70
+ }
90
71
 
91
- this.renderStack = newRenderStack;
92
- };
72
+ setOffsetProjectionEnabled(value: boolean) {
73
+ this.engagedIndicesTracker.enableOffsetProjection = value;
74
+ }
93
75
 
94
76
  updateProps(props: RecyclerViewProps<T>) {
95
- this.props = props;
77
+ this.propsRef = props;
96
78
  this.engagedIndicesTracker.drawDistance =
97
79
  props.drawDistance ?? this.engagedIndicesTracker.drawDistance;
98
- if (this.props.drawDistance === 0) {
80
+ if (this.propsRef.drawDistance === 0) {
99
81
  this.initialDrawBatchSize = 1;
100
82
  } else {
101
83
  this.initialDrawBatchSize = (props.numColumns ?? 1) * 2;
102
84
  }
85
+ this.initialDrawBatchSize =
86
+ this.propsRef.overrideProps?.initialDrawBatchSize ??
87
+ this.initialDrawBatchSize;
103
88
  }
104
89
 
105
90
  /**
@@ -111,7 +96,7 @@ export class RecyclerViewManager<T> {
111
96
  offset: number,
112
97
  velocity?: Velocity
113
98
  ): ConsecutiveNumbers | undefined {
114
- if (this.layoutManager) {
99
+ if (this.layoutManager && !this._isDisposed) {
115
100
  const engagedIndices = this.engagedIndicesTracker.updateScrollOffset(
116
101
  offset - this.firstItemOffset,
117
102
  velocity,
@@ -126,10 +111,18 @@ export class RecyclerViewManager<T> {
126
111
  return undefined;
127
112
  }
128
113
 
114
+ updateAverageRenderTime(time: number) {
115
+ this.engagedIndicesTracker.averageRenderTime = time;
116
+ }
117
+
129
118
  getIsFirstLayoutComplete() {
130
119
  return this.isFirstLayoutComplete;
131
120
  }
132
121
 
122
+ disableRecycling(disable: boolean) {
123
+ this.renderStackManager.disableRecycling = disable;
124
+ }
125
+
133
126
  getLayout(index: number) {
134
127
  if (!this.layoutManager) {
135
128
  throw new Error(
@@ -139,6 +132,17 @@ export class RecyclerViewManager<T> {
139
132
  return this.layoutManager.getLayout(index);
140
133
  }
141
134
 
135
+ tryGetLayout(index: number) {
136
+ if (
137
+ this.layoutManager &&
138
+ index >= 0 &&
139
+ index < this.layoutManager.getLayoutCount()
140
+ ) {
141
+ return this.layoutManager.getLayout(index);
142
+ }
143
+ return undefined;
144
+ }
145
+
142
146
  // Doesn't include header / foot etc
143
147
  getChildContainerDimensions() {
144
148
  if (!this.layoutManager) {
@@ -150,7 +154,7 @@ export class RecyclerViewManager<T> {
150
154
  }
151
155
 
152
156
  getRenderStack() {
153
- return this.renderStack;
157
+ return this.renderStackManager.getRenderStack();
154
158
  }
155
159
 
156
160
  getWindowSize() {
@@ -170,10 +174,10 @@ export class RecyclerViewManager<T> {
170
174
  getMaxScrollOffset() {
171
175
  return Math.max(
172
176
  0,
173
- (this.props.horizontal
177
+ (this.propsRef.horizontal
174
178
  ? this.getChildContainerDimensions().width
175
179
  : this.getChildContainerDimensions().height) -
176
- (this.props.horizontal
180
+ (this.propsRef.horizontal
177
181
  ? this.getWindowSize().width
178
182
  : this.getWindowSize().height) +
179
183
  this.firstItemOffset
@@ -189,46 +193,43 @@ export class RecyclerViewManager<T> {
189
193
  this.engagedIndicesTracker.setScrollDirection(scrollDirection);
190
194
  }
191
195
 
196
+ resetVelocityCompute() {
197
+ this.engagedIndicesTracker.resetVelocityHistory();
198
+ }
199
+
192
200
  updateLayoutParams(windowSize: RVDimension, firstItemOffset: number) {
193
201
  this.firstItemOffset = firstItemOffset;
194
202
  const LayoutManagerClass = this.getLayoutManagerClass();
195
203
  if (
196
204
  this.layoutManager &&
197
205
  Boolean(this.layoutManager?.isHorizontal()) !==
198
- Boolean(this.props.horizontal)
206
+ Boolean(this.propsRef.horizontal)
199
207
  ) {
200
208
  throw new Error(
201
209
  "Horizontal prop cannot be toggled, you can use a key on FlashList to recreate it."
202
210
  );
203
211
  }
212
+ if (this._isLayoutManagerDirty) {
213
+ this.layoutManager = undefined;
214
+ this._isLayoutManagerDirty = false;
215
+ }
216
+ const layoutManagerParams: LayoutParams = {
217
+ windowSize,
218
+ maxColumns: this.propsRef.numColumns ?? 1,
219
+ horizontal: Boolean(this.propsRef.horizontal),
220
+ optimizeItemArrangement: this.propsRef.optimizeItemArrangement ?? true,
221
+ overrideItemLayout: this.overrideItemLayout,
222
+ getItemType: this.getItemType,
223
+ };
204
224
  if (!(this.layoutManager instanceof LayoutManagerClass)) {
205
225
  // console.log("-----> new LayoutManagerClass");
206
226
 
207
227
  this.layoutManager = new LayoutManagerClass(
208
- {
209
- windowSize,
210
- maxColumns: this.props.numColumns ?? 1,
211
- horizontal: Boolean(this.props.horizontal),
212
- optimizeItemArrangement: this.props.optimizeItemArrangement ?? true,
213
- overrideItemLayout: (index, layout) => {
214
- this.props?.overrideItemLayout?.(
215
- layout,
216
- this.props.data![index],
217
- index,
218
- this.props.numColumns ?? 1,
219
- this.props.extraData
220
- );
221
- },
222
- },
228
+ layoutManagerParams,
223
229
  this.layoutManager
224
230
  );
225
231
  } else {
226
- this.layoutManager.updateLayoutParams({
227
- windowSize,
228
- maxColumns: this.props.numColumns ?? 1,
229
- horizontal: Boolean(this.props.horizontal),
230
- optimizeItemArrangement: this.props.optimizeItemArrangement ?? true,
231
- });
232
+ this.layoutManager.updateLayoutParams(layoutManagerParams);
232
233
  }
233
234
  }
234
235
 
@@ -236,7 +237,7 @@ export class RecyclerViewManager<T> {
236
237
  return this.layoutManager !== undefined;
237
238
  }
238
239
 
239
- getVisibleIndices() {
240
+ computeVisibleIndices() {
240
241
  if (!this.layoutManager) {
241
242
  throw new Error(
242
243
  "LayoutManager is not initialized, visible indices are not unavailable"
@@ -274,9 +275,9 @@ export class RecyclerViewManager<T> {
274
275
  // Using higher buffer for masonry to avoid missing items
275
276
  this.itemViewabilityManager.shouldListenToVisibleIndices &&
276
277
  this.itemViewabilityManager.updateViewableItems(
277
- this.props.masonry
278
+ this.propsRef.masonry
278
279
  ? this.engagedIndicesTracker.getEngagedIndices().toArray()
279
- : this.getVisibleIndices().toArray()
280
+ : this.computeVisibleIndices().toArray()
280
281
  );
281
282
  }
282
283
 
@@ -290,7 +291,7 @@ export class RecyclerViewManager<T> {
290
291
 
291
292
  processDataUpdate() {
292
293
  if (this.hasLayout()) {
293
- this.modifyChildrenLayout([], this.props.data?.length ?? 0);
294
+ this.modifyChildrenLayout([], this.propsRef.data?.length ?? 0);
294
295
  if (!this.recomputeEngagedIndices()) {
295
296
  // recomputeEngagedIndices will update the render stack if there are any changes in the engaged indices.
296
297
  // It's important to update render stack so that elements are assgined right keys incase items were deleted.
@@ -304,33 +305,46 @@ export class RecyclerViewManager<T> {
304
305
  }
305
306
 
306
307
  dispose() {
308
+ this._isDisposed = true;
307
309
  this.itemViewabilityManager.dispose();
308
310
  }
309
311
 
312
+ markLayoutManagerDirty() {
313
+ this._isLayoutManagerDirty = true;
314
+ }
315
+
310
316
  getInitialScrollIndex() {
311
317
  return (
312
- this.props.initialScrollIndex ??
313
- (this.props.maintainVisibleContentPosition?.startRenderingFromBottom
318
+ this.propsRef.initialScrollIndex ??
319
+ (this.propsRef.maintainVisibleContentPosition?.startRenderingFromBottom
314
320
  ? this.getDataLength() - 1
315
321
  : undefined)
316
322
  );
317
323
  }
318
324
 
325
+ shouldMaintainVisibleContentPosition() {
326
+ // Return true if maintainVisibleContentPosition is enabled and not horizontal
327
+ return (
328
+ !this.propsRef.maintainVisibleContentPosition?.disabled &&
329
+ !this.propsRef.horizontal
330
+ );
331
+ }
332
+
319
333
  getDataLength() {
320
- return this.props.data?.length ?? 0;
334
+ return this.propsRef.data?.length ?? 0;
321
335
  }
322
336
 
323
337
  private getLayoutManagerClass() {
324
338
  // throw errors for incompatible props
325
- if (this.props.masonry && this.props.horizontal) {
339
+ if (this.propsRef.masonry && this.propsRef.horizontal) {
326
340
  throw new Error("Masonry and horizontal props are incompatible");
327
341
  }
328
- if ((this.props.numColumns ?? 1) > 1 && this.props.horizontal) {
342
+ if ((this.propsRef.numColumns ?? 1) > 1 && this.propsRef.horizontal) {
329
343
  throw new Error("numColumns and horizontal props are incompatible");
330
344
  }
331
- return this.props.masonry
345
+ return this.propsRef.masonry
332
346
  ? RVMasonryLayoutManagerImpl
333
- : (this.props.numColumns ?? 1) > 1 && !this.props.horizontal
347
+ : (this.propsRef.numColumns ?? 1) > 1 && !this.propsRef.horizontal
334
348
  ? RVGridLayoutManagerImpl
335
349
  : RVLinearLayoutManagerImpl;
336
350
  }
@@ -344,7 +358,7 @@ export class RecyclerViewManager<T> {
344
358
  const initialItemLayout = this.layoutManager?.getLayout(
345
359
  initialScrollIndex ?? 0
346
360
  );
347
- const initialItemOffset = this.props.horizontal
361
+ const initialItemOffset = this.propsRef.horizontal
348
362
  ? initialItemLayout?.x
349
363
  : initialItemLayout?.y;
350
364
 
@@ -369,7 +383,7 @@ export class RecyclerViewManager<T> {
369
383
  const layoutManager = this.layoutManager;
370
384
  if (layoutManager) {
371
385
  this.applyInitialScrollAdjustment();
372
- const visibleIndices = this.getVisibleIndices();
386
+ const visibleIndices = this.computeVisibleIndices();
373
387
  // console.log("---------> visibleIndices", visibleIndices);
374
388
  this.hasRenderedProgressively = visibleIndices.every(
375
389
  (index) =>
@@ -390,7 +404,7 @@ export class RecyclerViewManager<T> {
390
404
  0,
391
405
  Math.min(
392
406
  visibleIndices.length,
393
- this.renderStack.size + this.initialDrawBatchSize
407
+ this.getRenderStack().size + this.initialDrawBatchSize
394
408
  )
395
409
  )
396
410
  );
@@ -399,14 +413,25 @@ export class RecyclerViewManager<T> {
399
413
 
400
414
  private getItemType(index: number): string {
401
415
  return (
402
- this.props.getItemType?.(this.props.data![index], index) ?? "default"
416
+ this.propsRef.getItemType?.(this.propsRef.data![index], index) ??
417
+ "default"
403
418
  ).toString();
404
419
  }
405
420
 
406
421
  private getStableId(index: number): string {
407
422
  return (
408
- this.props.keyExtractor?.(this.props.data![index], index) ??
423
+ this.propsRef.keyExtractor?.(this.propsRef.data![index], index) ??
409
424
  index.toString()
410
425
  );
411
426
  }
427
+
428
+ private overrideItemLayout(index: number, layout: SpanSizeInfo) {
429
+ this.propsRef?.overrideItemLayout?.(
430
+ layout,
431
+ this.propsRef.data![index],
432
+ index,
433
+ this.propsRef.numColumns ?? 1,
434
+ this.propsRef.extraData
435
+ );
436
+ }
412
437
  }
@@ -0,0 +1,291 @@
1
+ import { ConsecutiveNumbers } from "./helpers/ConsecutiveNumbers";
2
+
3
+ /**
4
+ * Manages the recycling of rendered items in a virtualized list.
5
+ * This class handles tracking, recycling, and reusing item keys to optimize
6
+ * rendering performance by minimizing creation/destruction of components.
7
+ */
8
+ export class RenderStackManager {
9
+ public disableRecycling = false;
10
+
11
+ // Maximum number of items that can be in the recycle pool
12
+ private maxItemsInRecyclePool: number;
13
+
14
+ // Stores pools of recycled keys for each item type
15
+ private recycleKeyPools: Map<string, Set<string>>;
16
+
17
+ // Maps active keys to their metadata (item type and stable ID)
18
+ private keyMap: Map<
19
+ string,
20
+ { itemType: string; index: number; stableId: string }
21
+ >;
22
+
23
+ // Maps stable IDs to their corresponding keys for quick lookups
24
+ private stableIdMap: Map<string, string>;
25
+
26
+ // Counter for generating unique sequential keys
27
+ private keyCounter: number;
28
+
29
+ /**
30
+ * @param maxItemsInRecyclePool - Maximum number of items that can be in the recycle pool
31
+ */
32
+ constructor(maxItemsInRecyclePool: number = Number.MAX_SAFE_INTEGER) {
33
+ this.maxItemsInRecyclePool = maxItemsInRecyclePool;
34
+ this.recycleKeyPools = new Map();
35
+ this.keyMap = new Map();
36
+ this.stableIdMap = new Map();
37
+ this.keyCounter = 0;
38
+ }
39
+
40
+ /**
41
+ * Synchronizes the render stack with the current state of data.
42
+ * This method is the core orchestrator that:
43
+ * 1. Recycles keys for items that are no longer valid
44
+ * 2. Updates existing keys for items that remain visible
45
+ * 3. Assigns new keys for newly visible items
46
+ * 4. Cleans up excess items to maintain the recycling pool size
47
+ *
48
+ * @param getStableId - Function to get a stable identifier for an item at a specific index
49
+ * @param getItemType - Function to get the type of an item at a specific index
50
+ * @param engagedIndices - Collection of indices that are currently visible or engaged
51
+ * @param dataLength - Total length of the data set
52
+ */
53
+ public sync(
54
+ getStableId: (index: number) => string,
55
+ getItemType: (index: number) => string,
56
+ engagedIndices: ConsecutiveNumbers,
57
+ dataLength: number
58
+ ) {
59
+ this.clearRecyclePool();
60
+
61
+ // Recycle keys for items that are no longer valid or visible
62
+ this.keyMap.forEach((keyInfo, key) => {
63
+ const { index, stableId, itemType } = keyInfo;
64
+ if (index >= dataLength) {
65
+ this.recycleKey(key);
66
+ return;
67
+ }
68
+ if (!engagedIndices.includes(index)) {
69
+ this.recycleKey(key);
70
+ return;
71
+ }
72
+ const newStableId = getStableId(index);
73
+ const newItemType = getItemType(index);
74
+ if (stableId !== newStableId || itemType !== newItemType) {
75
+ this.recycleKey(key);
76
+ }
77
+ });
78
+
79
+ // First pass: process items that already have optimized keys
80
+ for (const index of engagedIndices) {
81
+ if (this.hasOptimizedKey(getStableId(index))) {
82
+ this.syncItem(index, getItemType(index), getStableId(index));
83
+ }
84
+ }
85
+
86
+ // Second pass: process remaining items that need new keys
87
+ for (const index of engagedIndices) {
88
+ if (!this.hasOptimizedKey(getStableId(index))) {
89
+ this.syncItem(index, getItemType(index), getStableId(index));
90
+ }
91
+ }
92
+
93
+ // create indices that are not in the engagedIndices and less than dataLength
94
+ // select only indices that are not in the engagedIndices
95
+ const validIndicesInPool: number[] = [];
96
+ for (const keyInfo of this.keyMap.values()) {
97
+ const index = keyInfo.index;
98
+ if (index < dataLength && !engagedIndices.includes(index)) {
99
+ validIndicesInPool.push(index);
100
+ }
101
+ }
102
+
103
+ // First pass: process items that already have optimized keys
104
+ for (const index of validIndicesInPool) {
105
+ if (this.hasOptimizedKey(getStableId(index))) {
106
+ this.syncItem(index, getItemType(index), getStableId(index));
107
+ }
108
+ }
109
+
110
+ for (const index of validIndicesInPool) {
111
+ if (!this.hasOptimizedKey(getStableId(index))) {
112
+ this.syncItem(index, getItemType(index), getStableId(index));
113
+ }
114
+ }
115
+
116
+ // Clean up stale items and manage the recycle pool size
117
+ this.cleanup(getStableId, engagedIndices, dataLength);
118
+ }
119
+
120
+ /**
121
+ * Checks if a stable ID already has an assigned key
122
+ */
123
+ private hasOptimizedKey(stableId: string): boolean {
124
+ return this.stableIdMap.has(stableId);
125
+ }
126
+
127
+ /**
128
+ * Cleans up stale keys and manages the recycle pool size.
129
+ * This ensures we don't maintain references to items that are no longer in the dataset,
130
+ * and limits the number of recycled items to avoid excessive memory usage.
131
+ */
132
+ private cleanup(
133
+ getStableId: (index: number) => string,
134
+ engagedIndices: ConsecutiveNumbers,
135
+ dataLength: number
136
+ ) {
137
+ const itemsToDelete = new Array<string>();
138
+
139
+ // Remove items that are no longer in the dataset
140
+ for (const [key, keyInfo] of this.keyMap.entries()) {
141
+ const { index, itemType, stableId } = keyInfo;
142
+ if (index >= dataLength || getStableId(index) !== stableId) {
143
+ // TODO: Find a way to reusue the key, instead of deleting it
144
+ this.deleteKeyFromRecyclePool(itemType, key);
145
+ this.stableIdMap.delete(stableId);
146
+ itemsToDelete.push(key);
147
+ }
148
+ }
149
+
150
+ for (const key of itemsToDelete) {
151
+ this.keyMap.delete(key);
152
+ }
153
+
154
+ // Limit the size of the recycle pool
155
+ const itemsRenderedForRecycling = this.keyMap.size - engagedIndices.length;
156
+ if (itemsRenderedForRecycling > this.maxItemsInRecyclePool) {
157
+ const deleteCount =
158
+ itemsRenderedForRecycling - this.maxItemsInRecyclePool;
159
+ let deleted = 0;
160
+
161
+ // Use a for loop so we can break early once we've deleted enough items
162
+ const entries = Array.from(this.keyMap.entries()).reverse();
163
+ for (let i = 0; i < entries.length && deleted < deleteCount; i++) {
164
+ const [key, keyInfo] = entries[i];
165
+ const { index, itemType, stableId } = keyInfo;
166
+
167
+ if (!engagedIndices.includes(index)) {
168
+ this.deleteKeyFromRecyclePool(itemType, key);
169
+ this.stableIdMap.delete(stableId);
170
+ this.keyMap.delete(key);
171
+ deleted++;
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Places a key back into its type-specific recycle pool for future reuse
179
+ */
180
+ private recycleKey(key: string): void {
181
+ if (this.disableRecycling) {
182
+ return;
183
+ }
184
+ const keyInfo = this.keyMap.get(key);
185
+
186
+ if (!keyInfo) {
187
+ return;
188
+ }
189
+
190
+ const { itemType } = keyInfo;
191
+
192
+ // Add key back to its type's pool
193
+ const pool = this.getRecyclePoolForType(itemType);
194
+
195
+ pool.add(key);
196
+ }
197
+
198
+ /**
199
+ * Returns the current render stack containing all active keys and their metadata
200
+ */
201
+ public getRenderStack() {
202
+ return this.keyMap;
203
+ }
204
+
205
+ /**
206
+ * Syncs an individual item by assigning it an appropriate key.
207
+ * Will use an existing key if available, or generate a new one.
208
+ *
209
+ * @returns The key assigned to the item
210
+ */
211
+ private syncItem(index: number, itemType: string, stableId: string): string {
212
+ // Try to reuse an existing key, or get one from the recycle pool, or generate a new one
213
+ const newKey =
214
+ this.stableIdMap.get(stableId) ||
215
+ this.getKeyFromRecyclePool(itemType) ||
216
+ this.generateKey();
217
+
218
+ const keyInfo = this.keyMap.get(newKey);
219
+ if (keyInfo) {
220
+ // Update an existing key's metadata
221
+ this.deleteKeyFromRecyclePool(itemType, newKey);
222
+ this.deleteKeyFromRecyclePool(keyInfo.itemType, newKey);
223
+ this.stableIdMap.delete(keyInfo.stableId);
224
+ keyInfo.index = index;
225
+ keyInfo.itemType = itemType;
226
+ keyInfo.stableId = stableId;
227
+ } else {
228
+ // Create a new entry in the key map
229
+ this.keyMap.set(newKey, {
230
+ itemType,
231
+ index,
232
+ stableId,
233
+ });
234
+ }
235
+ this.stableIdMap.set(stableId, newKey);
236
+
237
+ return newKey;
238
+ }
239
+
240
+ /**
241
+ * Clears all recycled keys from the pool, effectively resetting the recycling system.
242
+ * This operation does not affect currently active keys.
243
+ */
244
+ private clearRecyclePool() {
245
+ // iterate over all pools and clear them
246
+ for (const pool of this.recycleKeyPools.values()) {
247
+ pool.clear();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Generates a unique sequential key using an internal counter.
253
+ * @returns A unique key as a string
254
+ */
255
+ private generateKey(): string {
256
+ return (this.keyCounter++).toString();
257
+ }
258
+
259
+ /**
260
+ * Removes a specific key from its type's recycle pool
261
+ */
262
+ private deleteKeyFromRecyclePool(itemType: string, key: string) {
263
+ this.recycleKeyPools.get(itemType)?.delete(key);
264
+ }
265
+
266
+ /**
267
+ * Gets or creates a recycle pool for a specific item type
268
+ */
269
+ private getRecyclePoolForType(itemType: string) {
270
+ let pool = this.recycleKeyPools.get(itemType);
271
+ if (!pool) {
272
+ pool = new Set();
273
+ this.recycleKeyPools.set(itemType, pool);
274
+ }
275
+ return pool;
276
+ }
277
+
278
+ /**
279
+ * Retrieves and removes a key from the type's recycle pool
280
+ * @returns A recycled key or undefined if none available
281
+ */
282
+ private getKeyFromRecyclePool(itemType: string) {
283
+ const pool = this.getRecyclePoolForType(itemType);
284
+ if (pool.size > 0) {
285
+ const key = pool.values().next().value;
286
+ pool.delete(key);
287
+ return key;
288
+ }
289
+ return undefined;
290
+ }
291
+ }