T
TanStack4mo ago
conscious-sapphire

Safe way to use the createRouter singleton without circular imports?

Hey all! Quick question, in a React + Vite app, is the router singleton (from createRouter) actually meant to be imported and used anywhere in the app? I’ve noticed that it can easily cause circular import issues — the singleton imports the route tree and the route tree is importing the page definitions (component). If a page then imports the singleton, it loops back and causes problems. Has anyone figured out the recommended way to use the router instance without running into these circular imports? Or if it is even recommended to export the router and use it as a singleton? Thanks in advance!
// Example showing circular import problem with TanStack Router in a single file context

// ❌ This util directly uses the router singleton
const someUtil = () => tanstackRouter.location

// Page component
export const AuditTrail: React.FC = () => {
// Using a util that references the router
const utilResult = someUtil();

return <div>Current location: {JSON.stringify(utilResult)}</div>;
};

// Route definition
export const Route = createFileRoute('/_authenticated/audit')({
component: AuditTrail,
});

// Router singleton
// ❌ Depends on the routeTree, which includes AuditTrail → creates circular import
export const tanstackRouter = createRouter({ routeTree });

// ---- Notes ----
// Import chain causing circular dependency:
// AuditTrail (page) -> someUtil -> tanstackRouter -> routeTree -> AuditTrail
// This is why importing the singleton inside pages or utils used by pages causes issues.
// Example showing circular import problem with TanStack Router in a single file context

// ❌ This util directly uses the router singleton
const someUtil = () => tanstackRouter.location

// Page component
export const AuditTrail: React.FC = () => {
// Using a util that references the router
const utilResult = someUtil();

return <div>Current location: {JSON.stringify(utilResult)}</div>;
};

// Route definition
export const Route = createFileRoute('/_authenticated/audit')({
component: AuditTrail,
});

// Router singleton
// ❌ Depends on the routeTree, which includes AuditTrail → creates circular import
export const tanstackRouter = createRouter({ routeTree });

// ---- Notes ----
// Import chain causing circular dependency:
// AuditTrail (page) -> someUtil -> tanstackRouter -> routeTree -> AuditTrail
// This is why importing the singleton inside pages or utils used by pages causes issues.
6 Replies
conscious-sapphire
conscious-sapphireOP4mo ago
Bump
rival-black
rival-black4mo ago
I dont think you should use the exported router anywhere else than to provide it for the RouterProvider. Any aspect of the router is available via hooks or any other API - useLocation, useRouter, useSearch etc. At least that is the case for file based routing.
optimistic-gold
optimistic-gold4mo ago
Actually, this is not true at all, the router instance is done like that, so in case you do needed you can export it and use it, at least it was mentioned in a question before. The problem with circular deps is a bit annoying, nothing you can do, other than structure your application better, i found that the hard way actually
rival-black
rival-black4mo ago
sure you can do it but then it seems like a very edge case. In my app there is not a single API I could not fetch with either any hooks or if its very specific with getRouteApiFunction https://tanstack.com/router/latest/docs/framework/react/api/router/getRouteApiFunction
getRouteApi function | TanStack Router React Docs
The getRouteApi function provides type-safe version of common hooks like useParams, useSearch, useRouteContext, useNavigate, useLoaderData, and useLoaderDeps that are pre-bound to a specific route ID...
conscious-sapphire
conscious-sapphireOP4mo ago
@Enyel the solution i've found to go around it is using an observable together with a proxy (to simplify use-cases). i'll attach an example. @dohomi this isn't really an edge case, in most router libraries the ability to use directly the router singleton is commonly used. Imagine you'd want to create a utility that does some kind of calculation and then navigates, you would want to have a way to just directly use the router singleton instead of being forced to pass it through an actual component.
// router-observable.tsx
export const routerObservable = new Observable<RegisteredRouter>();

export const tanstackRouter = new Proxy(
{},
{
get() {
return routerObservable.get();
},
set() {
throw new Error('tanstackRouter is read-only');
},
},
) as RegisteredRouter;


// tanstack-router.tsx
const createAppRouter = () =>
createRouter({
routeTree,
context: { theme: lightV2Theme },
defaultPreload: 'intent',
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
defaultStaleTime: moment.duration(1, 'h').asMilliseconds(),
defaultGcTime: moment.duration(2, 'h').asMilliseconds(),
defaultPendingMs: 100,
basepath: '/360',
defaultPendingComponent: () => <Spinner fullscreen />,
stringifySearch: customSearchSerializer,
});

const router = createAppRouter()

export const Router: React.FC<{ theme: Theme }> = ({ theme }) => {
return <RouterProvider context={{ theme }} router={router} />;
};

routerObservable.set(router);
// router-observable.tsx
export const routerObservable = new Observable<RegisteredRouter>();

export const tanstackRouter = new Proxy(
{},
{
get() {
return routerObservable.get();
},
set() {
throw new Error('tanstackRouter is read-only');
},
},
) as RegisteredRouter;


// tanstack-router.tsx
const createAppRouter = () =>
createRouter({
routeTree,
context: { theme: lightV2Theme },
defaultPreload: 'intent',
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
defaultStaleTime: moment.duration(1, 'h').asMilliseconds(),
defaultGcTime: moment.duration(2, 'h').asMilliseconds(),
defaultPendingMs: 100,
basepath: '/360',
defaultPendingComponent: () => <Spinner fullscreen />,
stringifySearch: customSearchSerializer,
});

const router = createAppRouter()

export const Router: React.FC<{ theme: Theme }> = ({ theme }) => {
return <RouterProvider context={{ theme }} router={router} />;
};

routerObservable.set(router);
this way whenever you use tanstackRouter it doesn't cause circular imports since you are not actually gathering it from the source that is passed to the Router itself
optimistic-gold
optimistic-gold4mo ago
No issues so far?

Did you find this page helpful?