Let's say we want to build a horizontal Week view, and we need to scroll in both directions. Scrolling to the left is relatively easy because FlatList
in react native has an onEndReached
event. That means that when the end is reached this triggers this event and we can add the next week to our list of dates which is stored in a state:
setDateList([...dateList, nextWeek])
,
The problem is when we must scroll to the left and show the past weeks. First, FlatList
doesn't have an onStartReached
event. But even if it was such an event how could we add a new week to the start of the list of data and expect to scroll to the left? IDK if I'm clear but to me makes no sense cause we will need at this point to rerender our FlatList and set the new data.
Anyway, there are plenty of solutions. Mine is like this:
We will use date-fns
library for dealing with dates: npm install date-fns --save
First, we will create a list of three weeks [last_week, current_week, next_week]
:
const d = eachWeekOfInterval(
{
start: subDays(new Date(), 14),
end: addDays(new Date(), 14),
},
{
weekStartsOn: 1,
}).reduce((acc: Date[][], cur) => {
const allDays = eachDayOfInterval({
start: cur,
end: addDays(cur, 6),
});
acc.push(allDays);
return ACC;
}, []);
then we will set this list as data to our FlatList
const [canMomentum, setCanMomentum] = useState(false);
const [dateList, setDateList] = useState<Date[][]>(d);
const ref = useRef<FlatList | null>(null);
const onMomentumScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (canMomentum) {
const index = Math.round(
event.nativeEvent.contentOffset.x / Layout.window.width
);
if (index === 0) {
const firstDayOfInterval = dateList[0][0];
const lastDayOfPastWeek = subDays(firstDayOfInterval, 1);
const firstDayOfPastWeek = startOfWeek(lastDayOfPastWeek, {
weekStartsOn: 1,
});
const pastWeek = setWeekInterval(firstDayOfPastWeek, lastDayOfPastWeek);
setDateList([pastWeek, ...dateList]);
ref.current?.scrollToIndex({ animated: false, index: 1 });
} else if (index === dateList.length - 1) {
const lastWeekOfInterval = dateList[dateList.length - 1];
const lastDayOfInterval =
lastWeekOfInterval[lastWeekOfInterval.length - 1];
const firstDayOFFutureWeek = addDays(lastDayOfInterval, 1);
const lastDayOfFutureWeek = endOfWeek(firstDayOFFutureWeek, {
weekStartsOn: 1,
});
const futureWeek = setWeekInterval(
firstDayOFFutureWeek,
lastDayOfFutureWeek
);
setDateList([...dateList, futureWeek]);
}
}
setCanMomentum(false);
};
const setWeekInterval = (start: Date, end: Date) => {
return eachDayOfInterval({
start,
end,
});
};
<FlatList
ref={ref}
showsHorizontalScrollIndicator={false}
pagingEnabled
horizontal
onScroll={(e) => {
setCanMomentum(true);
}}
initialScrollIndex={1}
onMomentumScrollEnd={onMomentumScrollEnd}
data={dateList}
keyExtractor={(_item: any, index: any) => index}
renderItem={({ item, index }: { item: any; index: number }) => (
<TestItem key={index} {...{ item }} />
)}
/>
Setting initialScrollIndex={1}
will initially show the current week in the FlatList
The line: ref.current?.scrollToIndex({ animated: false, index: 1 });
is the key. Once we scroll to the start of the list programmatically we tell the list to scroll to index 1, then add past weeks to the list setDateList([pastWeek, ...dateList]);
. In this way, we can scroll bidirectional. The only problem I have noticed is that when scrolling in the past there is a little blink.
Don't forget the ListItem should be wrapped inside a memo to avoid bad performance
interface Props {
item: Date[];
}
const TestItem: React.FC<Props> = ({ item }) => {
return (
<View
style={{
width: Layout.window.width,
alignItems: "center",
alignSelf: "stretch",
paddingVertical: 16,
flexDirection: "row",
}}
>
{item.map((item, index) => (
<View
key={index}
style={{ alignItems: "center", width: Layout.window.width / 7
}}>
<Subtitle1>{format(item, "dd")}</Subtitle1>
<Subtitle1>{format(item, "EEE")}</Subtitle1>
<Subtitle1>{format(item, "MMM")}</Subtitle1>
</View>
))}
</View>
);
};
export default memo(TestItem);
Hope it helps