diff --git a/.changeset/router-replace-option.md b/.changeset/router-replace-option.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/router-replace-option.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/ui/src/common/__tests__/withRedirect.test.tsx b/packages/ui/src/common/__tests__/withRedirect.test.tsx
index d0fb1c915ef..f70c11c4067 100644
--- a/packages/ui/src/common/__tests__/withRedirect.test.tsx
+++ b/packages/ui/src/common/__tests__/withRedirect.test.tsx
@@ -22,7 +22,25 @@ describe('withRedirect', () => {
render(, { wrapper });
- expect(fixtures.router.navigate).toHaveBeenCalledWith('/');
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('/', undefined);
+ });
+
+ it('forwards the navigate options to the redirect', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withUser({});
+ });
+
+ const WithHOC = withRedirect(
+ () => <>>,
+ () => true,
+ () => '/',
+ undefined,
+ { replace: true },
+ );
+
+ render(, { wrapper });
+
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('/', { replace: true });
});
it('does no redirects to the redirect url provided when the condition is not met', async () => {
@@ -39,6 +57,6 @@ describe('withRedirect', () => {
render(, { wrapper });
- expect(fixtures.router.navigate).not.toHaveBeenCalledWith('/');
+ expect(fixtures.router.navigate).not.toHaveBeenCalled();
});
});
diff --git a/packages/ui/src/common/withRedirect.tsx b/packages/ui/src/common/withRedirect.tsx
index 95758ac1bda..a2c1d13e932 100644
--- a/packages/ui/src/common/withRedirect.tsx
+++ b/packages/ui/src/common/withRedirect.tsx
@@ -18,6 +18,7 @@ export function withRedirect
(
condition: ComponentGuard,
redirectUrl: RedirectUrl,
warning?: string,
+ navigateOptions?: { replace?: boolean },
): (props: P) => null | JSX.Element {
const displayName = Component.displayName || Component.name || 'Component';
Component.displayName = displayName;
@@ -36,7 +37,7 @@ export function withRedirect
(
}
// TODO: Fix this properly
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- navigate(redirectUrl({ clerk, environment, options }));
+ navigate(redirectUrl({ clerk, environment, options }), navigateOptions);
}
}, []);
diff --git a/packages/ui/src/router/BaseRouter.tsx b/packages/ui/src/router/BaseRouter.tsx
index 8d401d38a4e..f4ce17d93b5 100644
--- a/packages/ui/src/router/BaseRouter.tsx
+++ b/packages/ui/src/router/BaseRouter.tsx
@@ -232,7 +232,7 @@ export const BaseRouter = ({
useHistoryChangeObserver(refreshEvents, observerRefresh);
// TODO: Look into the real possible types of globalNavigate
- const baseNavigate = async (toURL: URL | undefined): Promise => {
+ const baseNavigate = async (toURL: URL | undefined, { replace }: { replace?: boolean } = {}): Promise => {
if (!toURL) {
return;
}
@@ -243,7 +243,7 @@ export const BaseRouter = ({
if (isOutsideOfUIComponent || isCrossOrigin) {
isNavigatingRef.current = true;
try {
- return await clerkNavigate(toURL.href);
+ return await clerkNavigate(toURL.href, { replace });
} finally {
isNavigatingRef.current = false;
}
@@ -263,7 +263,7 @@ export const BaseRouter = ({
}
isNavigatingRef.current = true;
try {
- const internalNavRes = await internalNavigate(toURL, { metadata: { navigationType: 'internal' } });
+ const internalNavRes = await internalNavigate(toURL, { replace, metadata: { navigationType: 'internal' } });
// We need to flushSync to guarantee the re-render happens before handing things back to the caller,
// otherwise setActive might emit, and children re-render with the old navigation state.
// An alternative solution here could be to return a deferred promise, set that to state together
diff --git a/packages/ui/src/router/HashRouter.tsx b/packages/ui/src/router/HashRouter.tsx
index 1d1e2e8b452..2c3a79b6e0c 100644
--- a/packages/ui/src/router/HashRouter.tsx
+++ b/packages/ui/src/router/HashRouter.tsx
@@ -1,4 +1,5 @@
import { hasUrlInFragment, stripOrigin } from '@clerk/shared/internal/clerk-js/url';
+import type { NavigateOptions } from '@clerk/shared/types';
import React from 'react';
import type { RefreshEvent } from './BaseRouter';
@@ -14,11 +15,16 @@ interface HashRouterProps {
}
export const HashRouter = ({ preservedParams, children }: HashRouterProps): JSX.Element => {
- const internalNavigate = async (toURL: URL): Promise => {
+ const internalNavigate = async (toURL: URL, options?: NavigateOptions): Promise => {
if (!toURL) {
return;
}
- window.location.hash = stripOrigin(toURL).substring(1 + hashRouterBase.length);
+ const hash = stripOrigin(toURL).substring(1 + hashRouterBase.length);
+ if (options?.replace) {
+ window.location.replace('#' + hash);
+ } else {
+ window.location.hash = hash;
+ }
return Promise.resolve();
};
diff --git a/packages/ui/src/router/Route.tsx b/packages/ui/src/router/Route.tsx
index d331ad0ecf2..34dab5484f4 100644
--- a/packages/ui/src/router/Route.tsx
+++ b/packages/ui/src/router/Route.tsx
@@ -112,9 +112,9 @@ export function Route(props: RouteProps): JSX.Element | null {
return newGetMatchData(path, index) ? true : false;
},
resolve: resolve,
- navigate: (to: string, { searchParams } = {}) => {
+ navigate: (to: string, { searchParams, replace } = {}) => {
const toURL = resolve(to, { searchParams });
- return router.baseNavigate(toURL);
+ return router.baseNavigate(toURL, { replace });
},
refresh: router.refresh,
params: paramsDict,
diff --git a/packages/ui/src/router/RouteContext.tsx b/packages/ui/src/router/RouteContext.tsx
index 08da052e9c0..62354b94817 100644
--- a/packages/ui/src/router/RouteContext.tsx
+++ b/packages/ui/src/router/RouteContext.tsx
@@ -8,8 +8,8 @@ export interface RouteContextValue {
indexPath: string;
currentPath: string;
matches: (path?: string, index?: boolean) => boolean;
- baseNavigate: (toURL: URL) => Promise;
- navigate: (to: string, options?: { searchParams?: URLSearchParams }) => Promise;
+ baseNavigate: (toURL: URL, options?: { replace?: boolean }) => Promise;
+ navigate: (to: string, options?: { searchParams?: URLSearchParams; replace?: boolean }) => Promise;
resolve: (to: string) => URL;
refresh: () => void;
params: { [key: string]: string };
diff --git a/packages/ui/src/router/__tests__/HashRouter.test.tsx b/packages/ui/src/router/__tests__/HashRouter.test.tsx
index ab6ee1c17a8..87c698e0251 100644
--- a/packages/ui/src/router/__tests__/HashRouter.test.tsx
+++ b/packages/ui/src/router/__tests__/HashRouter.test.tsx
@@ -25,12 +25,12 @@ vi.mock('@clerk/shared/react', () => {
};
});
-const Button = ({ to, children }: React.PropsWithChildren<{ to: string }>) => {
+const Button = ({ to, replace, children }: React.PropsWithChildren<{ to: string; replace?: boolean }>) => {
const router = useRouter();
return (