Before submitting a new issue
Bug summary
The code:
return (
<CalendarContainer>
<CalendarHeader />
<CalendarBody />
</CalendarContainer>
)
The CalendarHeader and CalendarBody components can scroll independently on Android, causing the onChange callback to only fire when the body is scrolled, not when the header is scrolled. This issue appears to be random and is caused by a race condition in the scroll synchronization logic.
Description
When using CalendarContainer with CalendarHeader and CalendarBody as children, the header and body can become desynchronized on Android. When the user scrolls the header, the onChange callback does not fire, but when scrolling the body, it works correctly. This suggests that the scroll synchronization mechanism has a race condition that prevents the header's scroll events from being properly recognized.
Root Cause Analysis
The issue seems to be located in src/hooks/useSyncedList.tsx at lines 50-52:
const activeId = linkedScrollGroup.getActiveId() || ScrollType.calendarGrid;
if (activeId === id.toString() && visibleColumns && visibleDates) {
// Date change processing only happens if activeId matches
}
The Problem
-
Active ID Check: The code only processes date changes when activeId === id.toString(). This means:
- Header (
ScrollType.dayBar) only processes changes when activeId === 'dayBar'
- Body (
ScrollType.calendarGrid) only processes changes when activeId === 'calendarGrid'
-
Default Fallback: When getActiveId() returns null, it defaults to ScrollType.calendarGrid, which means:
- If the active ID is not set (null), only body scrolls are processed
- Header scrolls are ignored when active ID is null or still set to
calendarGrid
-
Race Condition on Android:
- The active ID is set via
onTouchStart handler in useLinkedScrollGroup.tsx (line 103-121)
- On Android,
onTouchStart events can be unreliable or delayed
- If a user quickly scrolls the header without a proper
onTouchStart event, the active ID may not be set to ScrollType.dayBar
- This causes the header's scroll events to be ignored
-
Touch Event Timing: The onTouchStart handler in useLinkedScrollGroup.tsx relies on touch events to set the active scroll ID:
const onTouchStartHandler = useCallback(
(triggerId: string) => {
// Sets activeId.current = triggerId
// Sets activeTag.value = elementTag
},
[activeTag, peers]
);
On Android, these touch events may not fire reliably before scroll events, causing the active ID to remain unset or incorrect.
Code References
Problematic Code
File: src/hooks/useSyncedList.tsx
- Lines: 50-52
- Issue: Restrictive active ID check prevents header scroll events from being processed
File: src/hooks/useLinkedScrollGroup/useLinkedScrollGroup.tsx
- Lines: 103-121
- Issue:
onTouchStart handler may not fire reliably on Android before scroll events
File: src/CalendarHeader.tsx
- Line: 75-78, 93-95
- Context: Header uses
ScrollType.dayBar and useSyncedList hook
File: src/CalendarBody.tsx
- Line: 104-107, 112-114
- Context: Body uses
ScrollType.calendarGrid and useSyncedList hook
Suggested Fix
The fix should ensure that date changes are processed from both scroll views, not just the "active" one. Here are two possible approaches:
Option 1: Remove Active ID Restriction (Recommended)
Modify useSyncedList.tsx to process date changes from both scroll views:
// Current (problematic):
const activeId = linkedScrollGroup.getActiveId() || ScrollType.calendarGrid;
if (activeId === id.toString() && visibleColumns && visibleDates) {
// Process date changes
}
// Suggested fix:
if (visibleColumns && visibleDates) {
// Process date changes regardless of active ID
// The linkedScrollGroup already handles scroll synchronization
// We just need to detect date changes from either scroll view
}
Option 2: Improve Active ID Detection
Ensure the active ID is set more reliably:
- Set active ID based on scroll events, not just touch events
- Add a fallback mechanism that checks which scroll view is actually moving
- Remove the default fallback to
ScrollType.calendarGrid to avoid bias
Option 3: Process Both Scroll Views
Allow both header and body to process date changes, but use the active ID only for scroll synchronization (not for date change detection):
// Process date changes from both views
// Use active ID only for determining which view initiated the scroll
const isActiveScroll = activeId === id.toString();
if (visibleColumns && visibleDates) {
// Always process date changes, but prioritize active scroll
if (isActiveScroll || !linkedScrollGroup.getActiveId()) {
// Process date changes
}
}
Priority
High - This affects core functionality (date change detection) and user experience on Android devices.
Library version
2.5.6
Environment info
- **Library**: `@howljs/calendar-kit` (version 2.5.6)
- **Platform**: Android (iOS behavior may differ)
- **React Native**: 0.76.9 (although reports include latests versions of the lib)
- **Reproducibility**: Random/Intermittent
Steps to reproduce
- Create a
CalendarContainer with CalendarHeader and CalendarBody as children
- On Android device/emulator, attempt to scroll the calendar by touching and dragging the header
- Observe that
onChange callback does not fire when scrolling the header
- Scroll the body instead -
onChange fires correctly
- The issue is intermittent - sometimes header scrolling works, sometimes it doesn't
Reproducible example repository
https://github.com/howljs/react-native-calendar-kit/
Before submitting a new issue
Bug summary
The code:
The
CalendarHeaderandCalendarBodycomponents can scroll independently on Android, causing theonChangecallback to only fire when the body is scrolled, not when the header is scrolled. This issue appears to be random and is caused by a race condition in the scroll synchronization logic.Description
When using
CalendarContainerwithCalendarHeaderandCalendarBodyas children, the header and body can become desynchronized on Android. When the user scrolls the header, theonChangecallback does not fire, but when scrolling the body, it works correctly. This suggests that the scroll synchronization mechanism has a race condition that prevents the header's scroll events from being properly recognized.Root Cause Analysis
The issue seems to be located in
src/hooks/useSyncedList.tsxat lines 50-52:The Problem
Active ID Check: The code only processes date changes when
activeId === id.toString(). This means:ScrollType.dayBar) only processes changes whenactiveId === 'dayBar'ScrollType.calendarGrid) only processes changes whenactiveId === 'calendarGrid'Default Fallback: When
getActiveId()returnsnull, it defaults toScrollType.calendarGrid, which means:calendarGridRace Condition on Android:
onTouchStarthandler inuseLinkedScrollGroup.tsx(line 103-121)onTouchStartevents can be unreliable or delayedonTouchStartevent, the active ID may not be set toScrollType.dayBarTouch Event Timing: The
onTouchStarthandler inuseLinkedScrollGroup.tsxrelies on touch events to set the active scroll ID:On Android, these touch events may not fire reliably before scroll events, causing the active ID to remain unset or incorrect.
Code References
Problematic Code
File:
src/hooks/useSyncedList.tsxFile:
src/hooks/useLinkedScrollGroup/useLinkedScrollGroup.tsxonTouchStarthandler may not fire reliably on Android before scroll eventsFile:
src/CalendarHeader.tsxScrollType.dayBaranduseSyncedListhookFile:
src/CalendarBody.tsxScrollType.calendarGridanduseSyncedListhookSuggested Fix
The fix should ensure that date changes are processed from both scroll views, not just the "active" one. Here are two possible approaches:
Option 1: Remove Active ID Restriction (Recommended)
Modify
useSyncedList.tsxto process date changes from both scroll views:Option 2: Improve Active ID Detection
Ensure the active ID is set more reliably:
ScrollType.calendarGridto avoid biasOption 3: Process Both Scroll Views
Allow both header and body to process date changes, but use the active ID only for scroll synchronization (not for date change detection):
Priority
High - This affects core functionality (date change detection) and user experience on Android devices.
Library version
2.5.6
Environment info
Steps to reproduce
CalendarContainerwithCalendarHeaderandCalendarBodyas childrenonChangecallback does not fire when scrolling the headeronChangefires correctlyReproducible example repository
https://github.com/howljs/react-native-calendar-kit/