Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/elements/src/react/router/tanstack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ClerkHostRouter } from '@clerk/types';
import { useRouter } from '@tanstack/react-router';

// Assume you adapt or define this constant similarly; e.g., for TanStack Router v1.0+
import { usePathnameWithoutCatchAll } from '../utils/path-inference/tanstack'; // Assume you create/adapt this util for TanStack (e.g., strip catch-all params like [...slug] from pathname)

/**
* Clerk Elements router integration with TanStack Router.
*/
export const useTanStackRouter = (): ClerkHostRouter => {
const router = useRouter();
const pathname = router.location.pathname;
const searchString = router.location.search; // Raw search string for URLSearchParams
const inferredBasePath = usePathnameWithoutCatchAll(); // Adapt your custom util for TanStack routing

// TanStack Router always uses history APIs under the hood for SPA navigation, preserving state without full re-renders.
// No version check needed unless integrating with very early betas; assume support for v1.x+.
const canUseHistoryAPIs = typeof window !== 'undefined';

// Helper to create URLSearchParams from search string (mimics Next.js useSearchParams return type)
const getSearchParams = () => new URLSearchParams(searchString);

return {
mode: 'path',
name: 'TanStackRouter',
push: (path: string) => router.navigate({ to: path }),
replace: (path: string) =>
canUseHistoryAPIs ? window.history.replaceState(null, '', path) : router.navigate({ to: path, replace: true }),
shallowPush: (path: string) =>
// In TanStack Router, all navigations are "shallow" by default (no full reload, preserves state).
// Use standard push; if you need to avoid re-fetching data, integrate with TanStack Query's stale-while-revalidate or disable refetch.
canUseHistoryAPIs ? window.history.pushState(null, '', path) : router.navigate({ to: path }),
pathname: () => pathname,
searchParams: () => getSearchParams(),
inferredBasePath: () => inferredBasePath,
};
};
53 changes: 53 additions & 0 deletions packages/elements/src/react/utils/path-inference/tanstack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useParams, useRouter } from '@tanstack/react-router';
import React from 'react';

import { removeOptionalCatchAllSegment } from './utils';

/**
* This hook grabs the current pathname and removes any (optional) catch-all segments.
* Adapted from Next.js App Router logic for TanStack Router.
* @example
* Route: /user/$[id]/profile/$[...rest] (or file: user.[id].profile.[[...rest]].tsx)
* Pathname: /user/123/profile/security
* Params: { id: '123', rest: ['security'] }
* Returns: /user/123/profile
* @returns The pathname without any catch-all segments
*/
export const usePathnameWithoutCatchAll = (): string => {
const router = useRouter();
const pathname = router?.location.pathname || ''; // Equivalent to usePathname()

// Early return for no router (SSR initial or error)
if (!pathname) {
return '/';
}

// Equivalent to useParams() – gets params for the current (leaf) route, which includes catch-alls
const params = useParams() as Record<string, string | string[] | undefined>; // Typed as needed

return React.useMemo(() => {
// Apply optional catch-all heuristic first (mirrors Next.js fallback)
const processedPath = removeOptionalCatchAllSegment(pathname);

// For resolved pathnames in TanStack: Split into parts (exclude leading /)
const pathParts = processedPath.split('/').filter(Boolean);

// Identify catch-all params: Those that are arrays (splats like [...rest])
const catchAllParams = Object.values(params || {})
.filter((v): v is string[] => Array.isArray(v))
.flat(Infinity); // Flatten all (handles multiple/nested, though rare)

// If no catch-all segments, return full path
if (catchAllParams.length === 0) {
return pathname.replace(/\/$/, '') || '/'; // Normalize trailing slash
}

// Slice off the trailing segments matching the catch-all length
// E.g., pathParts = ['user', '123', 'profile', 'security'], length=1 → slice(0, 3) = /user/123/profile
const baseParts = pathParts.slice(0, pathParts.length - catchAllParams.length);
const basePath = `/${baseParts.join('/')}`;

// Normalize: Ensure absolute and no trailing slash unless root
return basePath.replace(/\/$/, '') || '/';
}, [pathname, params]); // Dependencies: Recompute on navigation or param changes
};