Reputation: 435
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>
);
};
Upvotes: 3
Views: 5073
Reputation: 73
If you are using React.memo() for memorizing your renderItem component and passing props to it the right way (passing functions if any, using useCallback() so they are not written into memory on each render and cause unnecessary re render), even then updating parent state will cause re render if you will append new item at the start of list, if you append item at the end of list, it preserves previous items state and will not re render whole list.
Upvotes: 0
Reputation: 435
So, one of the way to solve a problem is to check props via React.memo
in Grid
component to prevent unnecessary months render. Grid
component in my case is whole month to render, so if I check for startDate
or endDate
in a month it will render only that months.
The problem is only when user wants to select the startDate
and endDate
within 5+ months difference. There will cause 5 renders again. Let's imagine if it will be 10+ months difference, it will start freezing.
Upvotes: 0
Reputation: 202706
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!!).
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
/>
Upvotes: 3
Reputation: 28400
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!
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 remove setDates
from the dependency list and add dates
.
const handleDayPress= useCallback(..., [dates]);
The memo looks good! It only needs futureYearRange
renderItem
- remove the current depencies and add handleDayPress
and monthsToRender
const renderItem = useCallback(..., [handleDayPress, monthsToRender]);
Upvotes: 0