@lazerlen/legend-calendar 1.4.1 → 1.6.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazerlen/legend-calendar",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "A better calendar for React Native.",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "@babel/preset-typescript": "^7.28.5",
29
29
  "@lazerlen/eslint-config": "*",
30
30
  "@lazerlen/tsconfig": "*",
31
- "@legendapp/list": "^3.0.0-beta.39",
31
+ "@legendapp/list": "^3.0.0",
32
32
  "@testing-library/react-hooks": "^8.0.1",
33
33
  "@types/bun": "^1.3.2",
34
34
  "@types/react": "~19.1.10",
@@ -42,8 +42,8 @@
42
42
  "typescript": "~5.9.2"
43
43
  },
44
44
  "peerDependencies": {
45
- "@legendapp/list": ">=3.0.0-beta.39",
45
+ "@legendapp/list": ">=3.0.0",
46
46
  "react": "*",
47
47
  "react-native": "*"
48
48
  }
49
- }
49
+ }
@@ -1,4 +1,4 @@
1
- import { memo, useEffect } from "react";
1
+ import { useEffect } from "react";
2
2
  import type { ColorSchemeName, PressableProps } from "react-native";
3
3
 
4
4
  import type {
@@ -13,6 +13,7 @@ import type { CalendarItemEmptyProps } from "@/components/CalendarItemEmpty";
13
13
  import { CalendarItemEmpty } from "@/components/CalendarItemEmpty";
14
14
  import type { CalendarItemWeekNameProps } from "@/components/CalendarItemWeekName";
15
15
  import { CalendarItemWeekName } from "@/components/CalendarItemWeekName";
16
+ import { useCalendarListConfig } from "@/components/CalendarListConfigContext";
16
17
  import type { CalendarRowMonthProps } from "@/components/CalendarRowMonth";
17
18
  import { CalendarRowMonth } from "@/components/CalendarRowMonth";
18
19
  import type { CalendarRowWeekProps } from "@/components/CalendarRowWeek";
@@ -105,7 +106,7 @@ export interface CalendarProps extends UseCalendarParams {
105
106
  CalendarPressableComponent?: PressableLike;
106
107
  }
107
108
 
108
- const BaseCalendar = memo(function BaseCalendar(props: CalendarProps) {
109
+ const BaseCalendar = function BaseCalendar(props: CalendarProps) {
109
110
  const {
110
111
  calendarInstanceId,
111
112
  calendarRowVerticalSpacing = 8,
@@ -185,16 +186,23 @@ const BaseCalendar = memo(function BaseCalendar(props: CalendarProps) {
185
186
  ))}
186
187
  </VStack>
187
188
  );
188
- });
189
+ };
190
+
191
+ export const Calendar = function Calendar(props: CalendarProps) {
192
+ const listConfig = useCalendarListConfig();
193
+
194
+ // When inside CalendarList, merge context values as defaults.
195
+ // Direct props always take precedence over context.
196
+ const resolvedProps = listConfig ? { ...listConfig, ...props } : props;
189
197
 
190
- export const Calendar = memo(function Calendar(props: CalendarProps) {
191
198
  const {
192
199
  calendarInstanceId,
193
200
  calendarActiveDateRanges,
194
201
  calendarMonthId,
195
202
  calendarColorScheme,
196
203
  ...otherProps
197
- } = props;
204
+ } = resolvedProps;
205
+
198
206
  useEffect(() => {
199
207
  // When used inside CalendarList, calendarActiveDateRanges is undefined
200
208
  // because CalendarList writes directly to the store. Skip to avoid
@@ -204,16 +212,6 @@ export const Calendar = memo(function Calendar(props: CalendarProps) {
204
212
  calendarInstanceId ?? "legend-calendar-default-instance",
205
213
  calendarActiveDateRanges
206
214
  );
207
- /**
208
- * While `calendarMonthId` is not used by the effect, we still need it in
209
- * the dependency array since [LegendList uses recycling
210
- * internally](https://legendapp.com/open-source/list/).
211
- *
212
- * This means `Calendar` can re-render with different props instead of
213
- * getting re-mounted. Without it, we would see staled/invalid data, as
214
- * reported by
215
- * [#11](https://github.com/MarceloPrado/flash-calendar/issues/11).
216
- */
217
215
  }, [calendarActiveDateRanges, calendarInstanceId, calendarMonthId]);
218
216
 
219
217
  return (
@@ -225,4 +223,4 @@ export const Calendar = memo(function Calendar(props: CalendarProps) {
225
223
  />
226
224
  </CalendarThemeProvider>
227
225
  );
228
- });
226
+ };
@@ -1,5 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
- import { memo } from "react";
3
2
  import type { TextStyle, ViewStyle } from "react-native";
4
3
  import { Pressable, StyleSheet, View } from "react-native";
5
4
 
@@ -161,7 +160,7 @@ export interface CalendarItemDayProps {
161
160
  * `CalendarItemDayWithContainer`, since it also includes the spacing between
162
161
  * each day.
163
162
  */
164
- export const CalendarItemDay = memo(function CalendarItemDay({
163
+ export const CalendarItemDay = function CalendarItemDay({
165
164
  onPress,
166
165
  children,
167
166
  theme,
@@ -295,7 +294,7 @@ export const CalendarItemDay = memo(function CalendarItemDay({
295
294
  }}
296
295
  </CalendarPressableComponent>
297
296
  );
298
- });
297
+ };
299
298
 
300
299
  interface CalendarItemDayContainerTheme {
301
300
  /** An empty view that acts as a spacer between each day. The spacing is
@@ -331,7 +330,7 @@ export interface CalendarItemDayContainerProps {
331
330
  metadata?: CalendarDayMetadata;
332
331
  }
333
332
 
334
- export const CalendarItemDayContainer = memo(function CalendarItemDayContainer({
333
+ export const CalendarItemDayContainer = function CalendarItemDayContainer({
335
334
  children,
336
335
  isStartOfWeek,
337
336
  shouldShowActiveDayFiller,
@@ -389,7 +388,7 @@ export const CalendarItemDayContainer = memo(function CalendarItemDayContainer({
389
388
  {children}
390
389
  </View>
391
390
  );
392
- });
391
+ };
393
392
 
394
393
  export interface CalendarItemDayWithContainerProps
395
394
  extends Omit<CalendarItemDayProps, "height">,
@@ -409,7 +408,7 @@ export interface CalendarItemDayWithContainerProps
409
408
  calendarInstanceId?: string;
410
409
  }
411
410
 
412
- export const CalendarItemDayWithContainer = memo(
411
+ export const CalendarItemDayWithContainer =
413
412
  function CalendarItemDayWithContainer({
414
413
  children,
415
414
  metadata: baseMetadata,
@@ -447,5 +446,4 @@ export const CalendarItemDayWithContainer = memo(
447
446
  </CalendarItemDay>
448
447
  </CalendarItemDayContainer>
449
448
  );
450
- }
451
- );
449
+ };
@@ -1,4 +1,3 @@
1
- import { memo } from "react";
2
1
  import type { ViewStyle } from "react-native";
3
2
  import { StyleSheet, View } from "react-native";
4
3
 
@@ -18,11 +17,11 @@ export interface CalendarItemEmptyProps {
18
17
  };
19
18
  }
20
19
 
21
- export const CalendarItemEmpty = memo(function CalendarItemEmpty(
20
+ export const CalendarItemEmpty = function CalendarItemEmpty(
22
21
  props: CalendarItemEmptyProps
23
22
  ) {
24
23
  const { height, theme } = props;
25
24
  const containerStyles = [{ ...styles.container, height }, theme?.container];
26
25
 
27
26
  return <View style={containerStyles} />;
28
- });
27
+ };
@@ -1,5 +1,4 @@
1
1
  import type { ReactNode } from "react";
2
- import { memo } from "react";
3
2
  import type { TextStyle, ViewStyle } from "react-native";
4
3
  import { StyleSheet, View } from "react-native";
5
4
 
@@ -35,7 +34,7 @@ export interface CalendarItemWeekNameProps {
35
34
  textProps?: Omit<CalendarTextProps, "children">;
36
35
  }
37
36
 
38
- export const CalendarItemWeekName = memo(function CalendarItemWeekName({
37
+ export const CalendarItemWeekName = function CalendarItemWeekName({
39
38
  children,
40
39
  height,
41
40
  theme,
@@ -57,4 +56,4 @@ export const CalendarItemWeekName = memo(function CalendarItemWeekName({
57
56
  </Text>
58
57
  </View>
59
58
  );
60
- });
59
+ };
@@ -3,11 +3,18 @@ import {
3
3
  type LegendListProps,
4
4
  type LegendListRef,
5
5
  } from "@legendapp/list/react-native";
6
- import { useEffect, useImperativeHandle, useRef } from "react";
6
+ import {
7
+ useEffect,
8
+ useImperativeHandle,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
7
13
  import { StyleSheet, View } from "react-native";
8
14
 
9
15
  import type { CalendarProps } from "@/components/Calendar";
10
16
  import { Calendar } from "@/components/Calendar";
17
+ import { CalendarListConfigProvider } from "@/components/CalendarListConfigContext";
11
18
  import {
12
19
  fromDateId,
13
20
  getWeekOfMonth,
@@ -27,11 +34,78 @@ const LegendList = LegendListBase as <T>(
27
34
  * `Calendar` props to simplify building custom `Calendar` components.
28
35
  */
29
36
  export type CalendarMonthEnhanced = CalendarMonth & {
37
+ /**
38
+ * The calendar configuration props for this item. Available when using a
39
+ * custom `renderItem` for backwards compatibility.
40
+ *
41
+ * @deprecated Prefer reading calendar config from context via
42
+ * `useCalendarListConfig()` instead of spreading `item.calendarProps`.
43
+ * This avoids creating a new data array each render and lets LegendList
44
+ * skip unnecessary item re-renders.
45
+ *
46
+ * **Before (slower):**
47
+ * ```tsx
48
+ * renderItem={({ item }) => (
49
+ * <MyCalendar calendarMonthId={item.id} {...item.calendarProps} />
50
+ * )}
51
+ * ```
52
+ *
53
+ * **After (faster):**
54
+ * ```tsx
55
+ * // Inside your custom calendar component:
56
+ * const listConfig = useCalendarListConfig();
57
+ * // Merge: { ...listConfig, ...props }
58
+ * ```
59
+ */
30
60
  calendarProps: Omit<CalendarProps, "calendarMonthId">;
31
61
  };
32
62
 
33
63
  const keyExtractor = (month: CalendarMonth) => month.id;
34
64
 
65
+ function buildCalendarConfig(
66
+ calendarColorScheme: CalendarMonthEnhanced["calendarProps"]["calendarColorScheme"],
67
+ calendarDayHeight: number,
68
+ calendarDisabledDateIds: CalendarMonthEnhanced["calendarProps"]["calendarDisabledDateIds"],
69
+ calendarFirstDayOfWeek: CalendarMonthEnhanced["calendarProps"]["calendarFirstDayOfWeek"],
70
+ calendarFormatLocale: CalendarMonthEnhanced["calendarProps"]["calendarFormatLocale"],
71
+ calendarInstanceId: CalendarMonthEnhanced["calendarProps"]["calendarInstanceId"],
72
+ calendarMaxDateId: CalendarMonthEnhanced["calendarProps"]["calendarMaxDateId"],
73
+ calendarMinDateId: CalendarMonthEnhanced["calendarProps"]["calendarMinDateId"],
74
+ calendarMonthHeaderHeight: number,
75
+ calendarRowHorizontalSpacing: CalendarMonthEnhanced["calendarProps"]["calendarRowHorizontalSpacing"],
76
+ calendarRowVerticalSpacing: number,
77
+ calendarWeekHeaderHeightProp: CalendarMonthEnhanced["calendarProps"]["calendarWeekHeaderHeight"],
78
+ getCalendarDayFormat: CalendarMonthEnhanced["calendarProps"]["getCalendarDayFormat"],
79
+ getCalendarMonthFormat: CalendarMonthEnhanced["calendarProps"]["getCalendarMonthFormat"],
80
+ getCalendarWeekDayFormat: CalendarMonthEnhanced["calendarProps"]["getCalendarWeekDayFormat"],
81
+ onCalendarDayPress: CalendarMonthEnhanced["calendarProps"]["onCalendarDayPress"],
82
+ theme: CalendarMonthEnhanced["calendarProps"]["theme"],
83
+ CalendarPressableComponent: CalendarMonthEnhanced["calendarProps"]["CalendarPressableComponent"]
84
+ ): CalendarMonthEnhanced["calendarProps"] {
85
+ const calendarWeekHeaderHeight =
86
+ calendarWeekHeaderHeightProp ?? calendarDayHeight;
87
+ return {
88
+ calendarColorScheme,
89
+ calendarDayHeight,
90
+ calendarDisabledDateIds,
91
+ calendarFirstDayOfWeek,
92
+ calendarFormatLocale,
93
+ calendarInstanceId,
94
+ calendarMaxDateId,
95
+ calendarMinDateId,
96
+ calendarMonthHeaderHeight,
97
+ calendarRowHorizontalSpacing,
98
+ calendarRowVerticalSpacing,
99
+ calendarWeekHeaderHeight,
100
+ getCalendarDayFormat,
101
+ getCalendarMonthFormat,
102
+ getCalendarWeekDayFormat,
103
+ onCalendarDayPress,
104
+ theme,
105
+ CalendarPressableComponent,
106
+ };
107
+ }
108
+
35
109
  export interface CalendarListProps
36
110
  extends Omit<CalendarProps, "calendarMonthId">,
37
111
  Omit<
@@ -100,6 +174,12 @@ export interface CalendarListProps
100
174
  * - calendarAdditionalHeight
101
175
  * - calendarRowVerticalSpacing
102
176
  * - calendarSpacing
177
+ *
178
+ * **Performance tip**: Using `item.calendarProps` is provided for
179
+ * backwards compatibility but creates a new data array each render.
180
+ * For better performance, have your custom component call
181
+ * `useCalendarListConfig()` to read the shared config from context
182
+ * and only use `item.id` as `calendarMonthId`.
103
183
  */
104
184
  renderItem?: LegendListProps<CalendarMonthEnhanced>["renderItem"];
105
185
  }
@@ -125,39 +205,37 @@ export interface CalendarListRef {
125
205
  scrollToOffset: (offset: number, animated: boolean) => void;
126
206
  }
127
207
 
128
- export function CalendarList({
129
- ref,
130
- ...props
131
- }: CalendarListProps & { ref?: React.Ref<CalendarListRef> }) {
208
+ type CalendarListInnerProps = CalendarListProps & {
209
+ ref?: React.Ref<CalendarListRef>;
210
+ } & {
211
+ flatListProps: Omit<
212
+ LegendListProps<CalendarMonthEnhanced>,
213
+ "renderItem" | "data" | "children"
214
+ >;
215
+ };
216
+
217
+ export function CalendarList(
218
+ props: CalendarListProps & { ref?: React.Ref<CalendarListRef> }
219
+ ) {
132
220
  const {
133
- // List-related props
221
+ ref,
134
222
  calendarInitialMonthId,
135
- calendarInitialScrollToActiveRange = true,
136
- calendarPastScrollRangeInMonths = 12,
137
- calendarFutureScrollRangeInMonths = 12,
138
- calendarFirstDayOfWeek = "sunday",
223
+ calendarInitialScrollToActiveRange,
224
+ calendarPastScrollRangeInMonths,
225
+ calendarFutureScrollRangeInMonths,
226
+ calendarFirstDayOfWeek,
139
227
  calendarFormatLocale,
140
-
141
- // Spacings
142
- calendarSpacing = 20,
228
+ calendarSpacing,
143
229
  calendarRowHorizontalSpacing,
144
- calendarRowVerticalSpacing = 8,
145
-
146
- // Heights
147
- calendarMonthHeaderHeight = 20,
148
- calendarDayHeight = 32,
149
- calendarWeekHeaderHeight = calendarDayHeight,
150
- calendarAdditionalHeight = 0,
151
-
152
- // Other props
230
+ calendarRowVerticalSpacing,
231
+ calendarMonthHeaderHeight,
232
+ calendarDayHeight,
233
+ calendarWeekHeaderHeight,
234
+ calendarAdditionalHeight,
153
235
  calendarColorScheme,
154
236
  theme,
155
237
  onEndReached,
156
238
  onStartReached,
157
- ...otherProps
158
- } = props;
159
-
160
- const {
161
239
  calendarActiveDateRanges,
162
240
  calendarDisabledDateIds,
163
241
  calendarInstanceId,
@@ -168,9 +246,82 @@ export function CalendarList({
168
246
  getCalendarWeekDayFormat,
169
247
  onCalendarDayPress,
170
248
  CalendarPressableComponent,
249
+ renderItem,
171
250
  ...flatListProps
172
- } = otherProps;
251
+ } = props;
252
+ return (
253
+ <CalendarListInner
254
+ CalendarPressableComponent={CalendarPressableComponent}
255
+ calendarActiveDateRanges={calendarActiveDateRanges}
256
+ calendarAdditionalHeight={calendarAdditionalHeight}
257
+ calendarColorScheme={calendarColorScheme}
258
+ calendarDayHeight={calendarDayHeight}
259
+ calendarDisabledDateIds={calendarDisabledDateIds}
260
+ calendarFirstDayOfWeek={calendarFirstDayOfWeek}
261
+ calendarFormatLocale={calendarFormatLocale}
262
+ calendarFutureScrollRangeInMonths={calendarFutureScrollRangeInMonths}
263
+ calendarInitialMonthId={calendarInitialMonthId}
264
+ calendarInitialScrollToActiveRange={calendarInitialScrollToActiveRange}
265
+ calendarInstanceId={calendarInstanceId}
266
+ calendarMaxDateId={calendarMaxDateId}
267
+ calendarMinDateId={calendarMinDateId}
268
+ calendarMonthHeaderHeight={calendarMonthHeaderHeight}
269
+ calendarPastScrollRangeInMonths={calendarPastScrollRangeInMonths}
270
+ calendarRowHorizontalSpacing={calendarRowHorizontalSpacing}
271
+ calendarRowVerticalSpacing={calendarRowVerticalSpacing}
272
+ calendarSpacing={calendarSpacing}
273
+ calendarWeekHeaderHeight={calendarWeekHeaderHeight}
274
+ flatListProps={flatListProps}
275
+ getCalendarDayFormat={getCalendarDayFormat}
276
+ getCalendarMonthFormat={getCalendarMonthFormat}
277
+ getCalendarWeekDayFormat={getCalendarWeekDayFormat}
278
+ onCalendarDayPress={onCalendarDayPress}
279
+ onEndReached={onEndReached}
280
+ onStartReached={onStartReached}
281
+ ref={ref}
282
+ renderItem={renderItem}
283
+ theme={theme}
284
+ />
285
+ );
286
+ }
173
287
 
288
+ function CalendarListInner({
289
+ ref,
290
+ // List-related props
291
+ calendarInitialMonthId,
292
+ calendarInitialScrollToActiveRange = true,
293
+ calendarPastScrollRangeInMonths = 12,
294
+ calendarFutureScrollRangeInMonths = 12,
295
+ calendarFirstDayOfWeek = "sunday",
296
+ calendarFormatLocale,
297
+ // Spacings
298
+ calendarSpacing = 20,
299
+ calendarRowHorizontalSpacing,
300
+ calendarRowVerticalSpacing = 8,
301
+ // Heights
302
+ calendarMonthHeaderHeight = 20,
303
+ calendarDayHeight = 32,
304
+ calendarWeekHeaderHeight: calendarWeekHeaderHeightProp,
305
+ calendarAdditionalHeight = 0,
306
+ // Other props
307
+ calendarColorScheme,
308
+ theme,
309
+ onEndReached,
310
+ onStartReached,
311
+ // Calendar config props
312
+ calendarActiveDateRanges,
313
+ calendarDisabledDateIds,
314
+ calendarInstanceId,
315
+ calendarMaxDateId,
316
+ calendarMinDateId,
317
+ getCalendarDayFormat,
318
+ getCalendarMonthFormat,
319
+ getCalendarWeekDayFormat,
320
+ onCalendarDayPress,
321
+ CalendarPressableComponent,
322
+ renderItem: customRenderItem,
323
+ flatListProps,
324
+ }: CalendarListInnerProps) {
174
325
  // Write directly to store to bypass the entire render cascade.
175
326
  // This means calendarProps stays stable and monthListWithCalendarProps
176
327
  // doesn't recompute on every date tap.
@@ -181,27 +332,57 @@ export function CalendarList({
181
332
  );
182
333
  }, [calendarActiveDateRanges, calendarInstanceId]);
183
334
 
184
- const calendarProps: CalendarMonthEnhanced["calendarProps"] = {
185
- // calendarActiveDateRanges intentionally omitted - written to store above
186
- calendarColorScheme,
187
- calendarDayHeight,
188
- calendarDisabledDateIds,
189
- calendarFirstDayOfWeek,
190
- calendarFormatLocale,
191
- calendarInstanceId,
192
- calendarMaxDateId,
193
- calendarMinDateId,
194
- calendarMonthHeaderHeight,
195
- calendarRowHorizontalSpacing,
196
- calendarRowVerticalSpacing,
197
- calendarWeekHeaderHeight,
198
- getCalendarDayFormat,
199
- getCalendarMonthFormat,
200
- getCalendarWeekDayFormat,
201
- onCalendarDayPress,
202
- theme,
203
- CalendarPressableComponent,
204
- };
335
+ // calendarActiveDateRanges intentionally omitted - written to store above.
336
+ // useMemo is required here: the React Compiler cannot cache the result of
337
+ // buildCalendarConfig because all its arguments flow through `tN === undefined
338
+ // ? default : tN` conditional expressions (the compiler's own pattern for
339
+ // defaulted props), which block cache slot generation.
340
+ const calendarProps = useMemo(
341
+ () =>
342
+ buildCalendarConfig(
343
+ calendarColorScheme,
344
+ calendarDayHeight,
345
+ calendarDisabledDateIds,
346
+ calendarFirstDayOfWeek,
347
+ calendarFormatLocale,
348
+ calendarInstanceId,
349
+ calendarMaxDateId,
350
+ calendarMinDateId,
351
+ calendarMonthHeaderHeight,
352
+ calendarRowHorizontalSpacing,
353
+ calendarRowVerticalSpacing,
354
+ calendarWeekHeaderHeightProp,
355
+ getCalendarDayFormat,
356
+ getCalendarMonthFormat,
357
+ getCalendarWeekDayFormat,
358
+ onCalendarDayPress,
359
+ theme,
360
+ CalendarPressableComponent
361
+ ),
362
+ [
363
+ calendarColorScheme,
364
+ calendarDayHeight,
365
+ calendarDisabledDateIds,
366
+ calendarFirstDayOfWeek,
367
+ calendarFormatLocale,
368
+ calendarInstanceId,
369
+ calendarMaxDateId,
370
+ calendarMinDateId,
371
+ calendarMonthHeaderHeight,
372
+ calendarRowHorizontalSpacing,
373
+ calendarRowVerticalSpacing,
374
+ calendarWeekHeaderHeightProp,
375
+ getCalendarDayFormat,
376
+ getCalendarMonthFormat,
377
+ getCalendarWeekDayFormat,
378
+ onCalendarDayPress,
379
+ theme,
380
+ CalendarPressableComponent,
381
+ ]
382
+ );
383
+
384
+ const calendarWeekHeaderHeight =
385
+ calendarWeekHeaderHeightProp ?? calendarDayHeight;
205
386
 
206
387
  const {
207
388
  initialMonthIndex,
@@ -218,29 +399,33 @@ export function CalendarList({
218
399
  calendarMinDateId,
219
400
  });
220
401
 
221
- // Compute the scroll index based on active date range if enabled
222
- let computedInitialScrollIndex = initialMonthIndex;
223
- if (calendarInitialScrollToActiveRange && calendarActiveDateRanges) {
224
- const firstRange = calendarActiveDateRanges[0];
225
- if (firstRange?.startId) {
226
- // Convert the startId to the first day of that month
227
- const startDate = fromDateId(firstRange.startId);
228
- const monthId = toDateId(startOfMonth(startDate));
229
-
230
- // Find the index of this month in the monthList
231
- const monthIndex = monthList.findIndex((month) => month.id === monthId);
232
-
233
- // Use this index if found, otherwise fall back to initialMonthIndex
234
- if (monthIndex !== -1) {
235
- computedInitialScrollIndex = monthIndex;
402
+ // Frozen after mount initialScrollIndex must not change after first render
403
+ // or LegendList re-renders its entire inner tree on every update.
404
+ // useState initializer runs exactly once on mount.
405
+ const [computedInitialScrollIndex] = useState(() => {
406
+ if (calendarInitialScrollToActiveRange && calendarActiveDateRanges) {
407
+ const firstRange = calendarActiveDateRanges[0];
408
+ if (firstRange?.startId) {
409
+ const startDate = fromDateId(firstRange.startId);
410
+ const monthId = toDateId(startOfMonth(startDate));
411
+ const monthIndex = monthList.findIndex((month) => month.id === monthId);
412
+ if (monthIndex !== -1) {
413
+ return monthIndex;
414
+ }
236
415
  }
237
416
  }
238
- }
417
+ return initialMonthIndex;
418
+ });
239
419
 
240
- const monthListWithCalendarProps = monthList.map((month) => ({
241
- ...month,
242
- calendarProps,
243
- }));
420
+ // Only build the enhanced list when user provides a custom renderItem
421
+ // (backwards-compat path). Otherwise use plain monthList so the data
422
+ // identity stays stable and LegendList can skip item re-renders.
423
+ const listData = customRenderItem
424
+ ? monthList.map((month) => ({
425
+ ...month,
426
+ calendarProps,
427
+ }))
428
+ : (monthList as unknown as CalendarMonthEnhanced[]);
244
429
 
245
430
  const handleOnEndReached = (info: { distanceFromEnd: number }) => {
246
431
  appendMonths(calendarFutureScrollRangeInMonths);
@@ -331,7 +516,7 @@ export function CalendarList({
331
516
 
332
517
  const calendarContainerStyle = { paddingBottom: calendarSpacing };
333
518
 
334
- const getFixedItemSize = (item: CalendarMonthEnhanced) => {
519
+ const getFixedItemSize = (item: CalendarMonth | CalendarMonthEnhanced) => {
335
520
  return getHeightForMonth({
336
521
  calendarMonth: item,
337
522
  calendarSpacing,
@@ -343,35 +528,37 @@ export function CalendarList({
343
528
  });
344
529
  };
345
530
 
531
+ // onCalendarDayPress is provided via CalendarListConfigContext at runtime,
532
+ // so we only need to pass calendarMonthId here.
346
533
  const handleRenderItem = ({ item }: { item: CalendarMonthEnhanced }) => (
347
534
  <View style={calendarContainerStyle}>
348
- <Calendar calendarMonthId={item.id} {...item.calendarProps} />
535
+ <Calendar
536
+ calendarMonthId={item.id}
537
+ onCalendarDayPress={onCalendarDayPress}
538
+ />
349
539
  </View>
350
540
  );
351
541
 
352
- // Uncertain why but passing this as a no op resolved blanking issues after adding
353
- // getFixedItemSize + recycleItems
354
- const handleViewableItemsChanged = () => {};
355
-
356
542
  return (
357
- <LegendList
358
- data={monthListWithCalendarProps}
359
- drawDistance={560}
360
- estimatedItemSize={273}
361
- getFixedItemSize={getFixedItemSize}
362
- initialScrollIndex={computedInitialScrollIndex}
363
- keyExtractor={keyExtractor}
364
- maintainVisibleContentPosition
365
- onEndReached={handleOnEndReached}
366
- onStartReached={handleOnStartReached}
367
- onViewableItemsChanged={handleViewableItemsChanged}
368
- recycleItems
369
- ref={legendListRef}
370
- renderItem={handleRenderItem}
371
- showsVerticalScrollIndicator={false}
372
- style={styles.container}
373
- {...flatListProps}
374
- />
543
+ <CalendarListConfigProvider value={calendarProps}>
544
+ <LegendList
545
+ data={listData}
546
+ drawDistance={560}
547
+ estimatedItemSize={273}
548
+ getFixedItemSize={getFixedItemSize}
549
+ initialScrollIndex={computedInitialScrollIndex}
550
+ keyExtractor={keyExtractor}
551
+ maintainVisibleContentPosition
552
+ onEndReached={handleOnEndReached}
553
+ onStartReached={handleOnStartReached}
554
+ recycleItems
555
+ ref={legendListRef}
556
+ renderItem={customRenderItem ?? handleRenderItem}
557
+ showsVerticalScrollIndicator={false}
558
+ style={styles.container}
559
+ {...flatListProps}
560
+ />
561
+ </CalendarListConfigProvider>
375
562
  );
376
563
  }
377
564
 
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import type { CalendarProps } from "@/components/Calendar";
4
+
5
+ /**
6
+ * The calendar configuration props shared across all items in a
7
+ * `CalendarList`. When `Calendar` is rendered inside `CalendarList`, it reads
8
+ * these values from context instead of receiving them through the list item's
9
+ * data, which keeps the `data` array identity stable and allows LegendList to
10
+ * skip unnecessary re-renders.
11
+ *
12
+ * When `Calendar` is used standalone (outside a list), the context is `null`
13
+ * and all props are passed directly.
14
+ */
15
+ export type CalendarListConfig = Omit<CalendarProps, "calendarMonthId">;
16
+
17
+ const CalendarListConfigContext = createContext<CalendarListConfig | null>(
18
+ null
19
+ );
20
+
21
+ export const CalendarListConfigProvider = CalendarListConfigContext.Provider;
22
+
23
+ /**
24
+ * Returns the shared calendar configuration from the nearest
25
+ * `CalendarListConfigProvider`, or `null` when used outside a `CalendarList`.
26
+ */
27
+ export const useCalendarListConfig = (): CalendarListConfig | null => {
28
+ return useContext(CalendarListConfigContext);
29
+ };