Skip to content

Commit 1384c67

Browse files
committed
feat: migrate from react router to tanstack router
1 parent e5fa3b4 commit 1384c67

31 files changed

+1019
-186
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ storybook-static/
3535
*.njsproj
3636
*.sln
3737
*.sw?
38+
39+
# Ai
40+
.claude/

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@package/ui": "workspace:*",
2929
"@tanstack/react-form": "1.23.7",
3030
"@tanstack/react-query": "5.83.0",
31+
"@tanstack/react-router": "^1.103.2",
3132
"clsx": "2.1.1",
3233
"immer": "10.1.1",
3334
"jotai": "2.13.1",
@@ -46,6 +47,7 @@
4647
"@package/storybook": "workspace:*",
4748
"@storybook/react": "catalog:storybook",
4849
"@tailwindcss/vite": "catalog:tailwind",
50+
"@tanstack/router-plugin": "^1.103.0",
4951
"@vitejs/plugin-basic-ssl": "catalog:vite",
5052
"@vitejs/plugin-react": "catalog:vite",
5153
"babel-plugin-react-compiler": "catalog:react19",

apps/web/src/classes/app-session/UseAppSessionContent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useMemo } from "react";
22
import { useFetchPersonalProfileQuery } from "@package/api";
3-
import { useNavigate } from "react-router-dom";
3+
import { useNavigate } from "@tanstack/react-router";
44
import type { IAppSessionContent } from "./IAppSessionContent";
55
import { useAuth } from "~/core/auth/UseAuth";
66

7-
export function useAppSessionContent(aboutRoute: string = "/about"): IAppSessionContent {
7+
export function useAppSessionContent(aboutRoute: string = "/version"): IAppSessionContent {
88
const navigate = useNavigate();
99
const { logout } = useAuth();
1010
const { data: profileInfo } = useFetchPersonalProfileQuery();
@@ -30,7 +30,7 @@ export function useAppSessionContent(aboutRoute: string = "/about"): IAppSession
3030
}
3131

3232
function handleOnAbout(): void {
33-
navigate(aboutRoute);
33+
navigate({ to: aboutRoute as "/version" });
3434
}
3535

3636
return {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type AuthStatus = "idle" | "authenticating" | "authenticated";
1+
export type AuthStatus = "idle" | "authenticating" | "authenticated" | "unauthenticated";

apps/web/src/core/App.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ReactElement, useEffect } from "react";
22
import { AppProviders } from "./AppProviders";
33
import { AppTheme } from "./AppTheme";
44
import { AppRoutes } from "./routes/AppRoutes";
5+
import { RouteLoading } from "./routes/logic/RouteLoading";
56

67
export function App(): ReactElement {
78
function handlePreloadError(): void {
@@ -18,10 +19,12 @@ export function App(): ReactElement {
1819
}, []);
1920

2021
return (
21-
<AppProviders>
22-
<AppTheme>
23-
<AppRoutes />
24-
</AppTheme>
22+
<AppProviders loadingFallback={<RouteLoading />}>
23+
{() => (
24+
<AppTheme>
25+
<AppRoutes />
26+
</AppTheme>
27+
)}
2528
</AppProviders>
2629
);
2730
}

apps/web/src/core/AppProviders.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import type { ReactElement } from "react";
1+
import type { ReactElement, ReactNode } from "react";
22
import { HeroUIProvider } from "@heroui/react";
33
import { ToastProvider } from "@heroui/toast";
44
import { validateCrypto } from "@package/react";
55
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
66
import { AppLocales } from "./AppLocales";
7+
import { AuthInitializer } from "./auth/AuthInitializer";
78
import { AuthProvider } from "./auth/AuthProvider";
89
import { PwaProviderModule } from "~/components/modules/pwa-provider-module/PwaProviderModule";
910

1011
const queryClient = new QueryClient();
1112

1213
export interface AppProvidersProps {
13-
children: ReactElement;
14+
children: (authStatus: "authenticated" | "unauthenticated") => ReactNode;
15+
loadingFallback?: ReactNode;
1416
}
1517

16-
export function AppProviders({ children }: AppProvidersProps): ReactElement {
18+
export function AppProviders({ children, loadingFallback }: AppProvidersProps): ReactElement {
1719
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
1820
typeof window !== "undefined" && validateCrypto();
1921
return (
@@ -22,7 +24,9 @@ export function AppProviders({ children }: AppProvidersProps): ReactElement {
2224
<AppLocales>
2325
<PwaProviderModule>
2426
<QueryClientProvider client={queryClient}>
25-
<AuthProvider>{children}</AuthProvider>
27+
<AuthInitializer fallback={loadingFallback}>
28+
{(authStatus) => <AuthProvider initialAuthStatus={authStatus}>{children(authStatus)}</AuthProvider>}
29+
</AuthInitializer>
2630
</QueryClientProvider>
2731
</PwaProviderModule>
2832
</AppLocales>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { ReactElement, ReactNode } from "react";
2+
import { useState, useEffect } from "react";
3+
import { usePostRefreshTokenMutate } from "@package/api";
4+
5+
export interface AuthInitializerProps {
6+
children: (authStatus: "authenticated" | "unauthenticated") => ReactNode;
7+
fallback?: ReactNode;
8+
}
9+
10+
/**
11+
* Component that initializes authentication by attempting to refresh the token.
12+
* Only renders children once auth status has been determined.
13+
*/
14+
export function AuthInitializer({ children, fallback = null }: AuthInitializerProps): ReactElement {
15+
const [authStatus, setAuthStatus] = useState<"authenticated" | "unauthenticated" | null>(null);
16+
const { mutateAsync: refreshToken } = usePostRefreshTokenMutate();
17+
18+
useEffect(() => {
19+
async function initializeAuth(): Promise<void> {
20+
try {
21+
// Attempt to refresh token on app load to check if user is logged in
22+
await refreshToken();
23+
setAuthStatus("authenticated");
24+
} catch {
25+
// If refresh fails, user is not authenticated
26+
setAuthStatus("unauthenticated");
27+
}
28+
}
29+
30+
void initializeAuth();
31+
}, [refreshToken]);
32+
33+
// Show fallback while determining auth status
34+
if (authStatus === null) {
35+
return <>{fallback}</>;
36+
}
37+
38+
// Once auth status is determined, render children with the status
39+
return <>{children(authStatus)}</>;
40+
}

apps/web/src/core/auth/AuthProvider.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
11
import type { ReactNode, ReactElement } from "react";
22
import { useReducer } from "react";
3-
import { usePostRefreshTokenMutate } from "@package/api";
4-
import { useRunOnce } from "@package/react";
53
import { AuthContext } from "./AuthContext";
64
import { AuthReducer } from "./AuthReducer";
75
import type { AuthState } from "~/classes/auth/AuthState";
86

97
export interface AuthProviderProps {
108
children: ReactNode;
9+
initialAuthStatus?: AuthState["status"];
1110
}
1211

1312
const initialState: AuthState = {
1413
status: "idle",
1514
};
1615

17-
export function AuthProvider({ children }: AuthProviderProps): ReactElement {
18-
const [state, dispatch] = useReducer(AuthReducer, initialState);
19-
const { mutateAsync: refreshToken } = usePostRefreshTokenMutate();
20-
21-
useRunOnce({
22-
func: async () => {
23-
try {
24-
// Run once on app load to check if the user is logged in
25-
await refreshToken();
26-
dispatch({ type: "auth/login" });
27-
} catch {
28-
dispatch({ type: "auth/logout" });
29-
}
30-
},
16+
export function AuthProvider({ children, initialAuthStatus = "idle" }: AuthProviderProps): ReactElement {
17+
const [state, dispatch] = useReducer(AuthReducer, {
18+
...initialState,
19+
status: initialAuthStatus,
3120
});
3221

3322
return (

apps/web/src/core/auth/AuthReducer.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { produce } from "immer";
22
import type { AuthState } from "~/classes/auth/AuthState";
33
import type { AuthStatus } from "~/classes/auth/AuthStatus";
44

5-
type AuthActions = { type: "auth/loading" } | { type: "auth/login" } | { type: "auth/logout" };
5+
type AuthActions =
6+
| { type: "auth/loading" }
7+
| { type: "auth/login" }
8+
| { type: "auth/logout" }
9+
| { type: "auth/initialized"; authenticated: boolean };
610

711
export interface AuthReducerProps extends AuthState {}
812

@@ -11,13 +15,16 @@ export function AuthReducer(baseState: AuthReducerProps, action: AuthActions): A
1115
return produce<AuthReducerProps>(baseState, (draft) => {
1216
switch (type) {
1317
case "auth/loading":
14-
draft.status = "idle";
18+
draft.status = "authenticating";
1519
break;
1620
case "auth/login":
1721
draft.status = "authenticated";
1822
break;
1923
case "auth/logout":
20-
draft.status = "authenticating";
24+
draft.status = "unauthenticated";
25+
break;
26+
case "auth/initialized":
27+
draft.status = action.authenticated ? "authenticated" : "unauthenticated";
2128
break;
2229
}
2330
});

apps/web/src/core/auth/UseAuth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useContext } from "react";
22
import type { LoginData } from "@package/api";
33
import { usePostLoginMutate, usePostLogoutMutate } from "@package/api";
4+
import { useNavigate, useRouter } from "@tanstack/react-router";
45
import { AuthContext } from "./AuthContext";
56
import type { AuthStatus } from "./AuthReducer";
67

@@ -11,6 +12,8 @@ export function useAuth(): {
1112
loginLoading: boolean;
1213
logoutLoading: boolean;
1314
} {
15+
const router = useRouter();
16+
const navigate = useNavigate();
1417
const {
1518
state: { status },
1619
dispatch,
@@ -30,6 +33,9 @@ export function useAuth(): {
3033
dispatch({
3134
type: "auth/logout",
3235
});
36+
router.invalidate().finally(() => {
37+
navigate({ to: "/" });
38+
});
3339
}
3440

3541
return {

0 commit comments

Comments
 (0)