-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Open
Labels
information neededFurther information is requestedFurther information is requested
Description
Which project does this relate to?
Start
Describe the bug
I have a kind of weird bug which is probably some misconfiguration on my side.
When running my Tanstack Start app the memory usage grows over time until it exceeds the limit, crashes and then restarts.
I did a heap snapshot and this is probably a issue with the queryClient.
I am not that used to bug/issue reporting. Please tell me which information you need and how I can make this as easy as possible for you
Your Example Website or App
Steps to Reproduce the Bug or Issue
I can try to provide a reproduction example if there is no initial dumb error in my code
Expected behavior
Memory should be more or less stable
Screenshots or Videos
I can provide a link to the heap snapshots if needed.
Platform
- Router / Start Version: 1.133.27
- OS: not relevant
- Browser: [e.g. Chrome, Safari, Firefox]
- Browser Version: [e.g. 91.1]
- Bundler: vite (deployed with nitro and coolify to a VPS
- Bundler Version: 7.0.6
Additional context
My Setup:
import { QueryClient } from '@tanstack/react-query';
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query';
import { queryClientConfig } from './context/tanstack-query-context';
import 'leaflet/dist/leaflet.css';
import { configureWebClient, getWebClient } from '@nono/api-client';
import * as Sentry from '@sentry/tanstackstart-react';
import { z } from 'zod';
import GeneralError from './features/errors/general-error';
import NotFoundError from './features/errors/not-found-error';
import { routeTree } from './routeTree.gen';
import { loadCookiesFn } from './utils/cookies';
z.config(z.locales.de());
configureWebClient({
baseUrl: import.meta.env.VITE_API_URL,
credentials: 'include',
});
// Global flag to prevent interceptor registration on every SSR request
let interceptorRegistered = false;
// Add cookies per-request (needed for SSR - server needs to forward cookies from incoming request)
// On the server, loadCookiesFn() reads from request headers; on client, it reads from browser cookies
if (!interceptorRegistered) {
getWebClient().interceptors.request.use(async (request) => {
const cookies = loadCookiesFn();
if (cookies) {
// Merge with existing Cookie header if present
const existingCookie = request.headers.get('Cookie');
if (existingCookie) {
request.headers.set('Cookie', `${existingCookie}; ${cookies}`);
} else {
request.headers.set('Cookie', cookies);
}
}
return request;
});
interceptorRegistered = true;
}
export function getRouter() {
const queryClient = new QueryClient(queryClientConfig);
const router = createTanStackRouter({
routeTree,
context: {
queryClient,
user: null,
session: null,
organizations: [],
avatarUrl: null,
},
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
defaultErrorComponent: () => <GeneralError />,
defaultNotFoundComponent: () => <NotFoundError />,
scrollRestoration: true,
});
if (!router.isServer) {
Sentry.init({
...
});
}
setupRouterSsrQueryIntegration({
router,
queryClient,
});
return router;
}
export const Route = createRootRouteWithContext<RouterContext>()({
validateSearch: z.object({
preview: z.boolean().optional(),
}),
beforeLoad: async ({ context }) => {
const session = await prefetchSession(context.queryClient);
return {
user: session.data?.user ?? null,
session: session.data?.session ?? null,
};
},
head: () => ({
...
// Detect if running on server (SSR) or client
const isServer = typeof window === 'undefined';
export const queryClientConfig: QueryClientConfig = {
defaultOptions: {
queries: {
// dehydrate: { serializeData: superjson.serialize },
// hydrate: { deserializeData: superjson.deserialize },
staleTime: STALE_TIME,
// Aggressive GC on server to prevent memory accumulation across SSR requests
// Client can keep longer cache for better UX
gcTime: isServer ? 1000 * 60 * 1 : 1000 * 60 * 10, // 1min SSR, 10min client
placeholderData: keepPreviousData,
retry: (failureCount, error) => {
if (failureCount >= 2) return false;
if (failureCount >= 0 && import.meta.env.DEV) return false;
if (error.message?.includes('401') || error.message?.includes('403'))
return false;
return true;
},
// refetchOnWindowFocus: import.meta.env.PROD,
// retry: 0,
refetchOnWindowFocus: false,
},
mutations: {
// Shorter GC time on server for mutations as well
gcTime: isServer ? 1000 * 60 * 1 : 1000 * 60 * 5, // 1min SSR, 5min client
onError: (error) => {
const schemaCheck = ApiResponseFailureSchema.safeParse(error);
if (schemaCheck.success) {
if (schemaCheck.data.error instanceof ZodError) {
toast.error(
z.prettifyError(schemaCheck.data.error) ??
'An unknown error occurred'
);
return;
}
toast.error(schemaCheck.data.error.title, {
description: schemaCheck.data.error.detail,
});
return;
}
toast.error(error.message ?? 'An unknown error occurred');
},
},
},
}
The way i use beforeLoad and loader (beforeLoad for guarding and loader for data prefetching)
export const Route = createFileRoute('/manage')({
component: RouteComponent,
beforeLoad: async ({ context }) => {
if (!context.user || !context.session) {
throw redirect({ to: '/auth/sign-in' });
}
const organizationId = context.session.activeOrganizationId;
if (!organizationId) {
throw redirect({ to: '/' });
}
// Pre-fetch organization and member in parallel
const [
organizationResponse,
organizationSettingsResponse,
venuesResponse,
eventsResponse,
] = await Promise.all([
context.queryClient.ensureQueryData(
getOrganizationOptions({
path: { organizationId },
})
),
context.queryClient.ensureQueryData(
getOrganizationSettingsOptions({
path: { organizationId },
})
),
context.queryClient.ensureQueryData(
listVenuesOptions({
query: { organizationId, pageIndex: 0, pageSize: 10 },
})
),
context.queryClient.ensureQueryData(
listEventsOptions({
query: { organizationId, pageIndex: 0, pageSize: 10 },
})
),
]);
const organization = {
...organizationResponse.data,
metadata: organizationResponse.data.metadata,
};
return {
user: context.user,
session: context.session,
organizationId,
organization,
organizationSettings: organizationSettingsResponse.data,
venues: venuesResponse.data.items,
events: eventsResponse.data.items,
logoUrl: organization.imageUrl ?? null,
mainEntry: {
label: organization.name || m.organization_info_title(),
href: '/manage/dashboard',
} as HeaderBreadcrumbItem,
};
},
loader: async () => {
return {
sidebarReferenceContext: {
type: 'organization',
id: null,
name: null,
} as SidebarReferenceContext,
};
},
});
export const Route = createFileRoute('/manage/venues/')({
component: RouteComponent,
validateSearch: zListVenuesData.shape.query
.omit({ organizationId: true })
.nonoptional(),
loaderDeps: ({ search }) => search,
loader: async ({ context, deps }) => {
await context.queryClient.ensureQueryData(
listVenuesOptions({
query: {
...deps,
organizationId: context.organizationId,
},
})
);
return {
organizationId: context.organizationId,
breadcrumb: [
{
label: m.venues_title(),
href: '/manage/venues',
},
] as HeaderBreadcrumbItem[],
};
},
head: ({ match }) => ({
meta: createMetadata({
title: m.venues_title(),
url: match.pathname.toString(),
}),
}),
});
coderabbitai and Jaime02
Metadata
Metadata
Assignees
Labels
information neededFurther information is requestedFurther information is requested