You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native实现Airbnb日历:如何阻止子组件重复渲染提升性能?

问题根源

你的CalendarDayBox虽然用了React.memo,但以下几个原因导致它还是会重复渲染:

  1. handleDateSelect依赖selectedDates状态:每次选中日期变化时,selectedDates更新,handleDateSelect会被重新创建,传递给所有CalendarDayBox的handleDateSelect prop引用改变,触发重渲染。
  2. getDayStyles、isWithinRange、isStartOrEndDate均依赖selectedDates:这些函数每次状态更新都会重新生成,传递给子组件的函数引用变化,导致React.memo的浅比较失效。
  3. dayStyles是循环内创建的新对象:即使样式内容不变,每次循环都会生成新的对象,React.memo会认为prop变化,触发重渲染。

优化方案

1. 使用函数式更新重构handleDateSelect

去掉对selectedDates的依赖,让函数引用保持稳定:

const handleDateSelect = useCallback((day, month, year) => {
  const date = format(new Date(year, month, day), 'yyyy-MM-dd');
  setSelectedDates(prev => {
    if (!prev.start) return {...prev, start: date};
    else if (!prev.end) return {...prev, end: date};
    else return {start: date, end: null};
  });
}, []); // 无依赖,引用始终稳定

2. 预计算所有日期的样式和状态数据

将原本在子组件中计算的dayStyles、isWithinRange、isStartOrEndDate结果提前计算好,避免传递函数给子组件,同时保证数据引用稳定:

// 预计算每个日期的样式映射
const dayStylesMap = useMemo(() => {
  const map = {};
  for (let day = 1; day <= daysInMonth; day++) {
    const date = format(new Date(year, month, day), 'yyyy-MM-dd');
    let styles;
    if (date === selectedDates.start || date === selectedDates.end) {
      styles = {backgroundColor: 'black', textColor: 'white'};
    } else if (selectedDates.start && selectedDates.end) {
      const isInRange = (date > selectedDates.start && date < selectedDates.end) || 
                        (date < selectedDates.start && date > selectedDates.end);
      styles = isInRange ? {backgroundColor: 'grey', textColor: 'black'} : 
                           {backgroundColor: 'transparent', textColor: 'black'};
    } else {
      styles = {backgroundColor: 'transparent', textColor: 'black'};
    }
    map[date] = styles;
  }
  return map;
}, [selectedDates, year, month, daysInMonth]);

// 预计算所有42个格子的完整数据
const dayData = useMemo(() => {
  const data = [];
  for (let idx = 0; idx < 42; idx++) {
    const dayNumber = getDayNumber(idx + 1);
    if (dayNumber === null) {
      data.push({
        dayNumber: null,
        isWithinRange: false,
        startEndStyles: {opacity: 0},
        styles: {backgroundColor: 'transparent', textColor: 'black'}
      });
      continue;
    }
    const date = format(new Date(year, month, dayNumber), 'yyyy-MM-dd');
    const isStart = date === selectedDates.start;
    const isEnd = date === selectedDates.end;
    const isWithin = isStart || isEnd || 
                    (selectedDates.start && selectedDates.end && 
                     ((date > selectedDates.start && date < selectedDates.end) || 
                      (date < selectedDates.start && date > selectedDates.end)));
    
    let startEndStyles = {opacity: 0};
    if (selectedDates.start && selectedDates.end) {
      if (!isStart && !isEnd) {
        const startOfRow = [0,7,14,21,28,35].includes(idx);
        const endOfRow = (dayNumber + dayOffset) % 7 === 0;
        startEndStyles = {
          borderTopLeftRadius: startOfRow ? 10 : 0,
          borderBottomLeftRadius: startOfRow ? 10 : 0,
          borderTopRightRadius: endOfRow ? 10 : 0,
          borderBottomRightRadius: endOfRow ? 10 : 0
        };
      } else {
        const rangeReversed = selectedDates.start > selectedDates.end;
        if (isStart && rangeReversed) {
          startEndStyles = {left:'-10%', borderTopRightRadius:50, borderBottomRightRadius:50};
        } else if (isEnd && rangeReversed) {
          startEndStyles = {left:'10%', borderTopLeftRadius:50, borderBottomLeftRadius:50};
        } else if (isStart) {
          startEndStyles = {left:'10%', borderTopLeftRadius:50, borderBottomLeftRadius:50};
        } else if (isEnd) {
          startEndStyles = {left:'-10%', borderTopRightRadius:50, borderBottomRightRadius:50};
        }
      }
    }

    data.push({
      dayNumber,
      isWithinRange: isWithin,
      startEndStyles,
      styles: dayStylesMap[date]
    });
  }
  return data;
}, [selectedDates, year, month, daysInMonth, dayOffset, dayStylesMap]);

3. 修改CalendarDayBox接收预计算数据

不再传递函数,直接使用预计算好的结果,确保所有props都是稳定的引用或原始值:

const CalendarDayBox = React.memo(
  ({dayStyles, month, year, boxWidth, isWithinRange, dayNumber, startEndStyles, handleDateSelect}) => {
    return (
      <View style={{justifyContent: "center", alignItems: "center", flexGrow: 1, flexBasis: boxWidth, height: boxWidth, marginBottom: 1, position: "relative"}}>
        <View style={{opacity: isWithinRange ? 1 : 0, position: "absolute", backgroundColor: "gray", height: boxWidth - 10, width: `${boxWidth + 1}px`, ...startEndStyles}}></View>
        <TouchableHighlight disabled={dayNumber === null} onPress={() => handleDateSelect(Number(dayNumber), month, year)}>
          <View style={{borderRadius: 50, width: boxWidth - 10, height: boxWidth - 10, justifyContent: "center", alignItems: "center", backgroundColor: dayStyles.backgroundColor}}>
            <Text style={{textAlign: "center", color: dayStyles.textColor, lineHeight: 0, fontSize: 10, fontWeight: "bold"}}>{dayNumber}</Text>
          </View>
        </TouchableHighlight>
      </View>
    );
  }
);

4. 更新渲染循环使用预计算数据

{dayData.map((item, idx) => (
  <CalendarDayBox 
    key={idx} 
    dayStyles={item.styles} 
    month={month} 
    year={year} 
    boxWidth={boxWidth} 
    isWithinRange={item.isWithinRange} 
    dayNumber={item.dayNumber} 
    startEndStyles={item.startEndStyles} 
    handleDateSelect={handleDateSelect}
  />
))}

额外优化建议

  • 避免在渲染循环中调用format函数,尽量在预计算阶段完成所有日期格式化,减少重复计算。
  • 对于固定的月份名称映射,可以提取到组件外部,避免每次渲染都创建新对象。

内容的提问来源于stack exchange,提问作者ProEvilz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 10:05:52