Home > Blockchain >  Prevent rerender flatlist React Native
Prevent rerender flatlist React Native

Time:04-09

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>
);
};

enter image description here

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 or useCallback 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 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]);
    

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
/>
  • Related