reactjsapollo-clientreact-suspense

apollo useSuspenseQuery causes non-stop rerenders


I have the below component -LocationStats- running a suspense query. The query causes non stop re-renders. I can see it sends a query to the server, and the minute it gets a response, it launches the same query again and that goes on and on. If i remove the suspense query and use good ol fashioned query it behaves normally, it fetches once and that's it.

using latest apollo 3.11.8 and latest react. Why is it behaving like this?

export const LocationStats: React.FC<GeographyDataProps> = ({ locationId, initialMeasure }) => {
    const timeSeriesRequests = useMemo(() => {
        const now = DateTime.now();
        return Object.values(timeseriesMetadata).reduce<TimeSeriesRequestInput[]>((acc, value) => {
            return [
                ...acc,
                { id: locationId, name: value, fromDate: now.toISO(), toDate: now.toISO() },
                { id: locationId, name: value, fromDate: now.minus({ years: 5 }).toISO(), toDate: now.toISO() },
            ];
        }, []);
    }, [locationId]);
    const {
        data: {
            timeSeriesEndPoint: { getTimeSeries: entries },
        },
    } = useSuspenseQuery<LocationStatsQuery>(locationStatsQuery, {
        variables: {
            timeSeriesRequests,
        },
        fetchPolicy: "no-cache",
    });
    const allData = useMemo(() => {
        return Object.entries(timeseriesMetadata).reduce<Record<Measure | `current${Measure}`, Timeseries>>(
            (acc, [key], index) => {
                const dataIndex = index * 2;
                acc[key as Measure] = {
                    metadata: { label: "", description: "" },
                    data: entries[dataIndex].data,
                };
                acc[`current${key as Measure}`] = {
                    metadata: { label: "", description: "" },
                    data: entries[dataIndex + 1].data,
                };
                return acc;
            },
            {} as Record<Measure | `current${Measure}`, Timeseries>,
        );
    }, [entries]);
const [activeTab, setActiveTab] = useState<Measure>(initialMeasure ?? Measure.Load);
return (
        <>
            <div className="self-stretch px-2.5 pt-1.5 border-b border-theme-border-base/20 justify-start items-start gap-1.5 inline-flex">
                {Object.values(Measure).map(measure => (
                    <Tab key={measure} active={activeTab === measure} onTabActivated={() => setActiveTab(measure)}>
                        {measure}
                    </Tab>
                ))}
            </div>
            <div className="self-stretch grow shrink basis-0 p-3 flex-col justify-center items-start gap-3 flex">
                <TimeseriesChart title={activeTab} data={allData[activeTab].data} metadata={allData[activeTab].metadata} />
            </div>
        </>
    );

// component using the above
const Fallback = <LoadingIndicator text="Loading data. Please wait..." />;
export const DetailsPanel = ({ highlightedGeograhy, geographyType }: DetailsPanelProps) => {
    return (
        <div className="bg-gray-100 rounded border border-theme-border-base flex-col justify-start items-start inline-flex flex-auto">
            <div className="self-stretch h-10 p-3 rounded-tl rounded-tr border justify-start items-center gap-2.5 inline-flex">
                <div className="grow shrink basis-0 h-4 justify-start items-center gap-1.5 flex">
                    <div className="text-xs">{geographyType}</div>
                    <div className="grow shrink basis-0 text-xs font-bold">{highlightedGeograhy?.name}</div>
                </div>
                <div className="justify-start items-center gap-1.5 flex">
                    <div className="w-px self-stretch bg-[#d9d9d9]" />
                    <ToggleButton className="chevron_left rounded border " title="Previous" />
                    <ToggleButton className="navigate_next rounded border " title="Next" />
                    <ToggleButton className="before:icon !icon-close rounded border border-theme-border-darker" title="Close" />
                </div>
            </div>
            <Suspense fallback={Fallback}>
                <LocationStats locationId={highlightedGeograhy.id} initialMeasure={Measure.Capacity} />
            </Suspense>
        </div>
    );
};

Solution

  • Any use of React's Suspense means that your component will not fully mount until your component has finished suspending.
    That's not unique to Apollo Client, but a general suspense concept.

    That also means that your useMemo will not "persist" until after the initial successful render - and as a result, every time the component tries to pick up rendering after the request finished (but before suspense finished), your useMemo thinks it's in a "new" component render, starts with empty values, and returns new timestamps.

    That in return will pass different variables to useSuspenseQuery and kick off a new request - your component never leaves the "suspending" state and will never be fully mounted.

    The solution here is to move the useMemo out of this component into a parent component and passing down timeSeriesRequests as props into this component.