I have custom created calendar using Flatlist. In the parent component I have a state with starting date and ending date and Press handler function to update state when user presses on the date. The problem is every time when I press the date render function invokes every time.
The question is: How to keep state to change, but not rerender whole calendar again and again?
Parent component with FlatList.
interface Props {
arrivalDate: string | undefined;
departureDate: string | undefined;
onDayPress: (day: Date) => void;
futureYearRange?: number;
}
const CustomCalendarList: React.FC<Props> = ({
arrivalDate,
departureDate,
futureYearRange = 5,
onDayPress,
}) => {
const months = useMonths();
const [isLoading, setIsLoading] = useState(true);
const [dates, setDates] = useState({
endDate: arrivalDate,
startDate: departureDate,
});
const handleDayPress= useCallback((row:IRow) => (e?: GestureResponderEvent) => {
if (!dates.startDate || (dates.startDate && dates.endDate)) {
setDates({endDate: undefined, startDate: row.date});
} else {
setDates(prevState => ({...prevState, endDate: row.date}))
}
}, [setDates]);
const { grids, monthsToRender } = useMemo(() => {
const monthToRender = 11 - dayjs().month() futureYearRange;
const monthsToRender: Array<{ title: string; year: number }> = [];
const grids = [];
for (let i = 0; i < monthToRender; i ) {
const newGrid: Array<Array<IRow>> = [];
const date = dayjs().add(i, "month");
const daysInMonth = dayjs(date).daysInMonth();
const monthIndex = dayjs(date).month();
const year = dayjs(date).year();
monthsToRender.push({ title: months[monthIndex], year });
for (let j = 0; j < daysInMonth - 1; j ) {
let row = [];
// minus 1 because by default in dayjs start week day is sunday(index=0)
let startingIndex = j === 0 ? dayjs(date).startOf("month").day() - 1 : 0;
startingIndex = startingIndex === -1 ? 6 : startingIndex;
for (let k = startingIndex; k < 7; k ) {
if (!(j 1 > daysInMonth)) {
row[k] = {
day: j 1,
date: dayjs(date)
.date(j 1)
.format("YYYY-MM-DD"),
};
}
if (k === 6) {
newGrid.push(row);
} else {
j = 1;
}
}
}
grids.push(newGrid);
};
console.log('generated')
return {
grids,
monthsToRender
};
}, [futureYearRange]);
const renderItem = useCallback(({
item,
index,
}: ListRenderItemInfo<Array<Array<IRow>>>) => {
return (
<Grid
onPress={handleDayPress}
monthsToRender={monthsToRender}
grid={item}
gridIndex={index}
/>
);
}, [dates.startDate, dates.endDate]);
useEffect(() => {
const timeoutId = setTimeout(() => {
setIsLoading(false);
}, 300);
return () => {
clearTimeout(timeoutId);
};
}, []);
if (isLoading) {
return (
<View
style={css`
height: 90%;
justify-content: center;
align-items: center;
background: ${colors.primaryBg};
`}
>
<ActivityIndicator color={"blue"} size="large" />
</View>
);
}
return (
<Calendar>
<FlatList
data={grids}
showsVerticalScrollIndicator={false}
updateCellsBatchingPeriod={1000}
renderItem={renderItem}
maxToRenderPerBatch={3}
keyExtractor={() => uuidv4()}
/>
</Calendar>
);
};
CodePudding user response:
I think it might have to do with the dependencies you are defining for some of your useCallback
hooks. The second parameter to useCallback should be an array of variables you reference inside the hook. For example:
const foo = Math.random() > 0.5 ? 'bar' : 'bing';
const someCallback = useCallback(() => {
if (foo === 'bar') { ... }
}, [foo]); // you must list "foo" here because it's referenced in the callback
There is one exception: state setters are not required as they are guaranteed to never change::
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the
useEffect
oruseCallback
dependency list.
https://reactjs.org/docs/hooks-reference.html#usestate
const [foo, setFoo] = useState('bar');
const someCallback = useCallback(() => {
setFoo('bing');
}, []); // there is no need to put `setFoo` here - WOOHOO!
Fixing your code:
You have several situations where you are not listing all of the referenced variables, which can get you into a very undetermined situation.
handleDayPress
- you can removesetDates
from the dependency list and adddates
.const handleDayPress= useCallback(..., [dates]);
The memo looks good! It only needs
futureYearRange
renderItem
- remove the current depencies and addhandleDayPress
andmonthsToRender
const renderItem = useCallback(..., [handleDayPress, monthsToRender]);
CodePudding user response:
Issue
You are generating new React keys each time the component renders.
<FlatList
data={grids}
showsVerticalScrollIndicator={false}
updateCellsBatchingPeriod={1000}
renderItem={renderItem}
maxToRenderPerBatch={3}
keyExtractor={() => uuidv4()} // <-- new React key each render cycle!
/>
With non-stable keys React assumes these are all new elements and need to be mounted and rendered. Using the array index would be a better solution (don't do that though!!).
Solution
Add the generated GUID as a property that can then be extracted when rendering.
Example:
const { grids, monthsToRender } = useMemo(() => {
...
const grids = [];
for (let i = 0; i < monthToRender; i ) {
...
for (let j = 0; j < daysInMonth - 1; j ) {
...
for (let k = startingIndex; k < 7; k ) {
if (!(j 1 > daysInMonth)) {
row[k] = {
guid: uuidV4(), // <-- generate here
day: j 1,
date: dayjs(date)
.date(j 1)
.format("YYYY-MM-DD")
};
}
if (k === 6) {
newGrid.push(row);
} else {
j = 1;
}
}
}
grids.push(newGrid);
}
return {
grids,
monthsToRender
};
}, [futureYearRange]);
...
<FlatList
data={grids}
showsVerticalScrollIndicator={false}
updateCellsBatchingPeriod={1000}
renderItem={renderItem}
maxToRenderPerBatch={3}
keyExtractor={({ guid }) => guid} // <-- extract here
/>