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

React Native Calendar Agenda组件切换至过去日期时数据异常求助

React Native Agenda组件切换至过去日期时数据异常问题解决

问题描述

使用react-native-calendars的Agenda组件展示用户预约信息、标记有预约的日期时,从存在多个预约的日期切换到过去日期,数据会出现异常变化。

组件代码

/* eslint-disable no-nested-ternary */
import { useLazyQuery, useQuery } from "@apollo/client";
import dayjs from "dayjs";
import React, { useContext, useState } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { Agenda } from "react-native-calendars";
import { useDispatch } from "react-redux";
import { UserContext } from "../context/AuthContextProvider";
import { GET_USER_APPOINTMENTS } from "../graphql/query";
import testIDs from "../testIDs";
import {
  addMissingDates,
  parseAppointmentData,
} from "../utils/calendarDataParser";
import { leadSelected } from "../reducers/leadSlice";
import { appointmentSelected } from "../reducers/appointmentSlice";

function AppointmentCalendar({ route, navigation }) {
  const [items, setItems] = useState({});
  const { authUser } = useContext(UserContext);
  const [variables, setVariables] = useState({
    userIds: [authUser?.id],
    startDate: dayjs()?.startOf("month")?.format("YYYY-MM-DD"),
    endDate: dayjs()?.endOf("month")?.format("YYYY-MM-DD"),
  });
  const dispatch = useDispatch();
  const { loading: appointmentLoader } = useQuery(GET_USER_APPOINTMENTS, {
    variables: { ...variables },
    onCompleted: (res) => {
      setItems((prev) => ({
        ...prev,
        ...addMissingDates(
          parseAppointmentData(res?.getUserAppointment?.data),
          dayjs().format("YYYY-MM-DD")
        ),
      }));
    },
  });

  const loadItems = (day) => {
    setVariables({
      ...variables,
      startDate: dayjs(day.dateString)?.startOf("month")?.format("YYYY-MM-DD"),
      endDate: dayjs(day.dateString)?.endOf("month")?.format("YYYY-MM-DD"),
    });
  };

  const renderEmptyDate = () => (
    <View style={styles?.emptyDate}>
      <Text>No appointment scheduled!</Text>
    </View>
  );
  const RenderItem = React.memo((props) => {
    const { reservation, isFirst, dispatch, navigation, route, styles } = props;
    const fontSize = isFirst ? 16 : 14;
    const color = isFirst ? "black" : "#43515c";
    const appointmentColor =
      reservation.appointmentStatus === "SCHEDULED"
        ? "#1890ff"
        : reservation.appointmentStatus === "RESCHEDULED"
        ? "#13c2c2"
        : reservation.appointmentStatus === "CANCELLED"
        ? "#f5222d"
        : "#52c41a";
    return (
      <TouchableOpacity
        testID={testIDs.agenda.ITEM}
        style={[
          styles.item,
          { height: reservation.height, backgroundColor: appointmentColor },
        ]}
        onPress={() => {
          navigation.navigate("EditAppointment", {
            ...route?.params,
            mode: "EDIT",
          });
          dispatch(appointmentSelected(reservation?.appointment));
        }}
      >
        <Text style={{ fontSize, color: "#fff" }}>{reservation?.name}</Text>
      </TouchableOpacity>
    );
  }, []);
  const rowHasChanged = (r1, r2) => r1?.appointment?.id !== r2?.appointment?.id;

  const timeToDate = (time) => {
    const date = new Date(time);
    return date.toISOString().split("T")[0];
  };

  const timeToString = (time) => {
    const date = new Date(time);
    return dayjs(date)?.format("hh:mm A");
  };
  return (
    <View style={styles?.container}>
      <Agenda
        testID={testIDs?.agenda?.CONTAINER}
        items={items}
        loadItemsForMonth={loadItems}
        selected={new Date()}
        onDayChange={(day) => console.log(day.dateString)}
        renderItem={(reservation, isFirst) => (
          <RenderItem
            isFirst={isFirst}
            key={reservation?.appointment?.id}
            reservation={reservation}
            dispatch={dispatch}
            navigation={navigation}
            route={route}
            styles={styles}
          />
        )}
        renderEmptyDate={renderEmptyDate}
        rowHasChanged={rowHasChanged}
        showClosingKnob
      />
    </View>
  );
}

export default AppointmentCalendar;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    // backgroundColor: "#a9addf",
  },
  boxContainer: {
    backgroundColor: "#a9addf",
    paddingTop: 10,
    paddingBottom: 10,
  },
  listItem: {
    marginTop: 5,
    padding: 10,
    backgroundColor: "#FFF",
    width: "98%",
    flex: 1,
    alignSelf: "center",
    flexDirection: "row",
    borderRadius: 5,
  },
  viewRow: {
    flex: 1,
    flexDirection: "row",
  },
  textGray: {
    color: "gray",
    fontSize: 14,
    marginBottom: 2,
  },
  textCapitalGray: {
    color: "gray",
    fontSize: 16,
    marginBottom: 2,
  },
  item: {
    backgroundColor: "white",
    flex: 1,
    borderRadius: 5,
    padding: 10,
    marginRight: 10,
    marginTop: 17,
  },
  emptyDate: {
    height: 15,
    flex: 1,
    paddingTop: 30,
  },
});

辅助函数代码

export const parseAppointmentData = (input) => {
  try {
    const systemTimezone = dayjs?.tz?.guess();
    const output = {};
    input?.forEach((item) => {
      const date = dayjs(item?.startDatetime)
        ?.tz(systemTimezone)
        ?.format("YYYY-MM-DD");
      const time = dayjs(item?.startDatetime)
        ?.tz(systemTimezone)
        ?.format("HH:mm A");
      const name = `${time} - ${item?.summary}`;

      if (!output[date]) {
        output[date] = [];
      }

      output[date]?.push({
        name,
        height: 50,
        day: date,
        appointmentStatus: item?.appointmentStatus,
        appointment: item,
      });
    });

    return output;
  } catch (err) {
    console?.log(err);
  }
};

export function addMissingDates(json, date) {
  const systemTimezone = dayjs?.tz?.guess();
  const missingDates = [];
  // Get the starting date of the current month
  const startingDate = dayjs(date)?.tz(systemTimezone)?.startOf("month");

  // Get the ending date of the current month
  const endingDate = dayjs(date)?.tz(systemTimezone)?.endOf("month");

  // Initialize the current date with the starting date
  let currentDate = startingDate;
  const datesWithAppointments = Object?.keys(json);
  // Loop until the current date reaches the ending date
  while (currentDate?.isSameOrBefore(endingDate, "day")) {
    // Output the current date
    if (!datesWithAppointments?.includes(currentDate?.format("YYYY-MM-DD"))) {
      missingDates?.push(currentDate?.format("YYYY-MM-DD"));
    }
    // Increment the current date by one day
    currentDate = currentDate?.add(1, "day");
  }
  missingDates?.forEach((missedDate) => {
    const formattedDate = missedDate;
    json[formattedDate] = [];
  });

  return json;
}

问题根源

  1. 数据合并逻辑错误:onCompleted中使用...prev合并新老数据,导致切换月份时旧月份的预约数据不会被清除,新数据与旧数据混杂引发显示异常。
  2. 补全空日期参数错误:调用addMissingDates时传入的是当前日期,而非正在加载的目标月份日期,导致补全的是空日期为当前月份的,而非目标月份的,进一步加剧数据混乱。

解决方案

修改1:重置数据而非合并

在useQuery的onCompleted回调中,直接用新请求的处理后数据替换items,不再保留旧数据:

onCompleted: (res) => {
  // 基于当前请求的月份日期补全空日期
  const targetMonthDate = variables.startDate;
  const parsedData = parseAppointmentData(res?.getUserAppointment?.data);
  const newItems = addMissingDates(parsedData, targetMonthDate);
  // 直接替换items,而非合并旧数据
  setItems(newItems);
},

修改2:确保补全空日期的参数正确性

通过variables.startDate传入目标月份的起始日期,保证addMissingDates补全的是当前加载月份的所有空日期。

完整修改后的组件关键部分代码

const { loading: appointmentLoader } = useQuery(GET_USER_APPOINTMENTS, {
  variables: { ...variables },
  onCompleted: (res) => {
    const targetMonthDate = variables.startDate;
    const parsedData = parseAppointmentData(res?.getUserAppointment?.data);
    const newItems = addMissingDates(parsedData, targetMonthDate);
    setItems(newItems);
  },
});

额外优化建议

  • 添加月份缓存逻辑,避免重复请求相同月份的数据,提升性能。
  • 在loadItems中添加防抖处理,避免快速切换月份时频繁发起请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 03:14:50