I am creating a calendar component that contains 2 Views with FlatList
. Each View is associated with a different data source. Specifically, View 1 will display all days in a month where View 2 will display all days in a week.
Link to video demo, button of year is for view switching
For simplicity, each month/week view is represented by a date as an array item in a bigger array.
For example, each item in the month
array is a unique data in a month. An array of size 12 will have 12 unique dates of each month. Each Date()
object will then be passed down to my children components for a proper rendering. The result of this particular month
array is a list of complete calendar of 12 months.
Note: To improve performance, my child component will only render the current month -1
, current month
, and current month +1
, all other months will be rendered conditionally as soon as user scrolls away.
[
...
Wed May 30 2018 21:51:47 GMT+0800 (+08),
Sat Jun 30 2018 21:51:47 GMT+0800 (+08),
Mon Jul 30 2018 21:51:47 GMT+0800 (+08),
...
]
The same applies to View 2, which is the week view. Except that my data is now contained unique days in a week for all weeks in a month.
Before I implement this approach, I tried to hack something out from my child component. For instance, when users click a button to switch view, it would pass down a prop, with the current Date()
object to notify the children as to conditionally render a different view. This is basically switching between a View
and a FlatList
. Nested FlatList
was a bad idea as both lists are scrolling towards the same direction.
Not to mention that my calendar component involve jumping to selected month. Another solution I had implemented is to use only month
array for both views by hacking out the indexes from keyExtractor
. For example, I would reindex the list as soon as user jumps
from one month to another but soon I realized this is an anti-pattern an only causes more problem.
Note: I have implemented
shouldComponentUpdate
in all my children components. So scrolling in just 1 View should not be a problem, size of the data source will not affect at all because only the changes happen to the-1
,0
, and+1
month will be reflected. The bottleneck of my component is only happening when switching views.
Lost story short, now I have resorted to the current solution where I have 2 different data source (array
) for 2 different Views in a FlatList
. I have a button for the user to switch between modes. The problem is it takes some time for switching mode (setting states) as every time it re-instantiates the view by calling this prop onViewableItemsChanged
from FlatList
, which involve some complex calculation. Is there a better approach?
Code (Parent)
renderCalendarList = () => {
return (
<FlatList
pageSize={1}
horizontal={true}
pagingEnabled={true}
scrollEnabled={true}
keyExtractor={this.keyExtractor}
ref={(c) => this.calendarList = c}
getItemLayout={this.getItemLayout}
renderItem={this.renderCalendarComponent}
onViewableItemsChanged={this.onViewableItemsChanged}
data={(this.state.weekMode ? this.state.weekRows : this.state.rows)} />
)
}
switchView = () => {
this.setState({ weekMode: !this.state.weekMode });
// next week is going to be month view
if (this.state.weekMode) {
const mYClone = this.state.monthYears.slice(0);
const selectedDay = DateUtils.formatDateInMY(this.props.selectedDay);
for (let i = 0; i < mYClone.length; i++) {
if (selectedDay === mYClone[i]) {
this.setState({ currentMonth: this.props.selectedDay })
this.calendarList.scrollToIndex({ animated: false, index: i });
}
}
} else { // next week is going to be week view
const rowClone = this.state.weekRows.slice(0);
for (let i = 0; i < rowClone.length; i++) {
if (isSameWeek(rowClone[i], this.props.selectedDay)) {
this.setState({ currentMonth: rowClone[i] });
this.calendarList.scrollToIndex({ animated: false, index: i });
}
}
}
}
Code (Children)
render() {
const { container, monthContainer, weekContainer } = styles;
const { currentMonth, firstDay, style, weekMode } = this.props;
if (!weekMode) {
const days = DateUtils.populateMonth(currentMonth, firstDay);
const weeks = [];
while (days.length) {
weeks.push(this.renderWeek(days.splice(0, 7), weeks.length));
}
return (
<View style={[container, style]}>
<View style={[monthContainer]}>{weeks}</View>
</View>
)
} else {
const startDay = subDays(currentMonth, 3); // focus on middle
const endDay = addDays(startDay, 6)
const days = eachDay(startDay, endDay, 1);
const daysToRender = [];
days.forEach((day, dayID) => {
daysToRender.push(this.renderDay(day, dayID))
});
return (
<View style={style}>
<View style={[weekContainer]}>
{daysToRender}
</View>
</View>
)
}
}