Skip to content

Commit

Permalink
Rework the AppRouter component
Browse files Browse the repository at this point in the history
  • Loading branch information
patricklafrance committed Oct 21, 2023
1 parent f0a46f0 commit ba4fa3a
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 226 deletions.
5 changes: 5 additions & 0 deletions samples/endpoints/local-module/src/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ function registerRoutes(runtime: Runtime) {
to: "/subscription"
});

runtime.registerNavigationItem({
$label: "Public page",
to: "/public"
});

runtime.registerNavigationItem({
$label: "Tabs",
$priority: 100,
Expand Down
317 changes: 96 additions & 221 deletions samples/endpoints/shell/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,47 @@ import { useIsMswStarted } from "@squide/msw";
import { useIsAuthenticated, useIsRouteMatchProtected, useLogger, useRoutes, type Logger } from "@squide/react-router";
import { useAreModulesReady } from "@squide/webpack-module-federation";
import axios from "axios";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Outlet, RouterProvider, createBrowserRouter, useLocation } from "react-router-dom";

type SetSession = (session: Session) => void;
type SetSubscription = (subscription: Subscription) => void;
/*
AppRouter
- loader
- onFetchInitialData -> (doit passer un "signal")
- onFetchSession
- onFetchProtectedData -> Si fournie, est inclus dans le isReady - (doit passer un "signal")
- waitForMsw
- rootRoute - Si fournis est-ce le parent de la root route du AppRouter?
- routerProviderOptions
*/

/*
import { AppRouter as SquideAppRouter } from "@squide/shell";
export function AppRouter() {
const [subscription, setSubscription] = useState<Subscription>();
onFetchProtectedData() {
....
}
return (
<TelemetryContext.Provider value={}>
<SubcriptionContext.Provider value={subscription}
<SquideAppRouter onFetchProtectedData={onFetchProtectedData} />
</SubcriptionContext.Provider >
</TelemetryContext.Provider value={}>
)
}
*/

async function fetchProtectedData(
setSession: SetSession,
setSubscription: SetSubscription,
setSession: (session: Session) => void,
setSubscription: (subscription: Subscription) => void,
logger: Logger
) {
const sessionPromise = axios.get("/api/session")
Expand All @@ -25,7 +57,7 @@ async function fetchProtectedData(

logger.debug("[shell] %cSession is ready%c:", "color: white; background-color: green;", "", session);

setSession(data);
setSession(session);
});

const subscriptionPromise = axios.get("/api/subscription")
Expand All @@ -38,7 +70,7 @@ async function fetchProtectedData(

logger.debug("[shell] %cSubscription is ready%c:", "color: white; background-color: green;", "", subscription);

setSubscription(data);
setSubscription(subscription);
});

return Promise.all([sessionPromise, subscriptionPromise])
Expand All @@ -53,85 +85,29 @@ async function fetchProtectedData(
}

interface RootRouteProps {
setSession: SetSession;
setSubscription: SetSubscription;
}

export function RootRoute({ setSession, setSubscription }: RootRouteProps) {
const logger = useLogger();

const location = useLocation();
const telemetryService = useTelemetryService();

const isAuthenticated = useIsAuthenticated();
const isActiveRouteProtected = useIsRouteMatchProtected(location);

const [isReady, setIsReady] = useState(!isActiveRouteProtected || isAuthenticated);

useEffect(() => {
telemetryService?.track(`Navigated to the "${location.pathname}" page.`);

// If the user is already authenticated and come back later with a direct hit to a public page,
// without this code, once the user attempt to navigate to a protected page, the user will be asked
// to login again because the AppRouter code is not re-rendered when the location change.
// To try this out:
// - Authenticate to the app with temp/temp
// - Navigate to the /public page and force a full refresh
// - Click on "Go to the protected home page" link
// - If this code work, you should be redirected directly to the home page without having to login
// - If this code fail, you will be redirected to the login page
if (isActiveRouteProtected && !isAuthenticated) {
setIsReady(false);

logger.debug(`[shell] Fetching protected data as "${location}" is a protected route.`);

fetchProtectedData(setSession, setSubscription, logger).finally(() => {
setIsReady(true);
});
}
}, [location, telemetryService, logger, isAuthenticated, isActiveRouteProtected, setSession, setSubscription]);

if (!isReady) {
return <div>Loading...</div>;
}

return (
<Outlet />
);
}

export interface AppRouterProps {
waitForMsw: boolean;
sessionManager: SessionManager;
telemetryService: TelemetryService;
areModulesReady: boolean;
}

export function AppRouter({ waitForMsw, sessionManager, telemetryService }: AppRouterProps) {
const [isReady, setIsReady] = useState(false);
// Most of the bootstrapping logic has been moved to this component because AppRouter
// cannot leverage "useLocation" since it's depend on "RouterProvider".
export function RootRoute({ waitForMsw, sessionManager, areModulesReady }: RootRouteProps) {
const [isProtectedDataLoaded, setIsProtectedDataLoaded] = useState(false);

// Could be done with a ref (https://react.dev/reference/react/useRef) to save a re-render but for this sample
// it seemed unnecessary. If your application loads a lot of data at bootstrapping, it should be considered.
const [subscription, setSubscription] = useState<Subscription>();

const logger = useLogger();
const routes = useRoutes();

// Re-render the app once all the remotes are registered, otherwise the remotes routes won't be added to the router.
const areModulesReady = useAreModulesReady();
const location = useLocation();
const telemetryService = useTelemetryService();

// Re-render the app once MSW is started, otherwise, the API calls for module routes will return a 404 status.
const isMswStarted = useIsMswStarted(waitForMsw);

// Ideally "useLocation" would be used so the component re-renderer everytime the location change but it doesn't
// seem feasible (at least not easily) as public and private routes go through this component and we expect to show the same
// loading through the whole bootstrapping process.
// Anyhow, since all the Workleap apps will authenticate through a third party authentication provider, it
// doesn't seems like a big deal as the application will be reloaded anyway after the user logged in on the third party.
const isActiveRouteProtected = useIsRouteMatchProtected(window.location);

const setSession = useCallback((session: Session) => {
sessionManager.setSession(session);
}, [sessionManager]);
const isActiveRouteProtected = useIsRouteMatchProtected(location);
const isAuthenticated = useIsAuthenticated();

useEffect(() => {
if (areModulesReady && !isMswStarted) {
Expand All @@ -144,169 +120,68 @@ export function AppRouter({ waitForMsw, sessionManager, telemetryService }: AppR

if (areModulesReady && isMswStarted) {
if (isActiveRouteProtected) {
logger.debug(`[shell] Fetching protected data as "${window.location}" is a protected route.`);
if (!isAuthenticated) {
logger.debug(`[shell] Fetching protected data as "${location.pathname}" is a protected route.`);

fetchProtectedData(setSession, setSubscription, logger).finally(() => {
setIsReady(true);
});
} else {
logger.debug(`[shell] Passing through as "${window.location}" is a public route.`);
const setSession = (session: Session) => {
sessionManager.setSession(session);
};

setIsReady(true);
fetchProtectedData(setSession, setSubscription, logger).finally(() => {
setIsProtectedDataLoaded(true);
});
}
} else {
logger.debug(`[shell] Passing through as "${location.pathname}" is a public route.`);
}
}
}, [logger, areModulesReady, isMswStarted, isActiveRouteProtected, setSession]);
}, [logger, location, sessionManager, areModulesReady, isMswStarted, isActiveRouteProtected, isAuthenticated]);

useEffect(() => {
telemetryService?.track(`Navigated to the "${location.pathname}" page.`);
}, [location, telemetryService]);

if (!areModulesReady || !isMswStarted || (isActiveRouteProtected && !isProtectedDataLoaded)) {
return <div>Loading...</div>;
}

return (
<SubscriptionContext.Provider value={subscription}>
<Outlet />
</SubscriptionContext.Provider>
);
}

export interface AppRouterProps {
waitForMsw: boolean;
sessionManager: SessionManager;
telemetryService: TelemetryService;
}

export function AppRouter({ waitForMsw, sessionManager, telemetryService }: AppRouterProps) {
// Re-render the app once all the remotes are registered, otherwise the remotes routes won't be added to the router.
const areModulesReady = useAreModulesReady();

const routes = useRoutes();

const router = useMemo(() => {
return createBrowserRouter([
{
element: <RootRoute setSession={setSession} setSubscription={setSubscription} />,
element: (
<RootRoute
waitForMsw={waitForMsw}
sessionManager={sessionManager}
areModulesReady={areModulesReady}
/>
),
children: routes
}
]);
}, [routes, setSession, setSubscription]);

if (!isReady) {
return <div>Loading...</div>;
}
}, [areModulesReady, routes, waitForMsw, sessionManager]);

return (
<TelemetryServiceContext.Provider value={telemetryService}>
<SubscriptionContext.Provider value={subscription}>
<RouterProvider router={router} />
</SubscriptionContext.Provider>
<RouterProvider router={router} />
</TelemetryServiceContext.Provider>
);
}


// import { SubscriptionContext, TelemetryServiceContext, useTelemetryService, type Session, type SessionManager, type Subscription, type TelemetryService } from "@endpoints/shared";
// import { useIsMswStarted } from "@squide/msw";
// import { useIsRouteMatchProtected, useLogger, useRoutes } from "@squide/react-router";
// import { useAreModulesReady } from "@squide/webpack-module-federation";
// import axios from "axios";
// import { useEffect, useMemo, useState } from "react";
// import { Outlet, RouterProvider, createBrowserRouter, useLocation } from "react-router-dom";

// export function RootRoute() {
// const location = useLocation();
// const telemetryService = useTelemetryService();

// useEffect(() => {
// telemetryService?.track(`Navigated to the "${location.pathname}" page.`);
// }, [location, telemetryService]);

// return (
// <Outlet />
// );
// }

// export interface AppRouterProps {
// waitForMsw: boolean;
// sessionManager: SessionManager;
// telemetryService: TelemetryService;
// }

// export function AppRouter({ waitForMsw, sessionManager, telemetryService }: AppRouterProps) {
// const [isReady, setIsReady] = useState(false);

// // Could be done with a ref (https://react.dev/reference/react/useRef) to save a re-render but for this sample
// // it seemed unnecessary. If your application loads a lot of data at bootstrapping, it should be considered.
// const [subscription, setSubscription] = useState<Subscription>();

// const logger = useLogger();
// const routes = useRoutes();

// // Re-render the app once all the remotes are registered, otherwise the remotes routes won't be added to the router.
// const areModulesReady = useAreModulesReady();

// // Re-render the app once MSW is started, otherwise, the API calls for module routes will return a 404 status.
// const isMswStarted = useIsMswStarted(waitForMsw);

// // Ideally "useLocation" would be used so the component re-renderer everytime the location change but it doesn't
// // seem feasible (at least not easily) as public and private routes go through this component.
// // Anyhow, since all the Workleap apps will authenticate through a third party authentication provider, it
// // doesn't seems like a big deal as the application will be reloaded anyway after the user logged in on the third party.
// const isActiveRouteProtected = useIsRouteMatchProtected(window.location);

// useEffect(() => {
// if (areModulesReady && !isMswStarted) {
// logger.debug("[shell] Modules are ready, waiting for MSW to start.");
// }

// if (!areModulesReady && isMswStarted) {
// logger.debug("[shell] MSW is started, waiting for the modules to be ready.");
// }

// if (areModulesReady && isMswStarted) {
// if (isActiveRouteProtected) {
// logger.debug(`[shell] Fetching session data as "${window.location}" is a protected route.`);

// const sessionPromise = axios.get("/api/session")
// .then(({ data }) => {
// const session: Session = {
// user: {
// id: data.userId,
// name: data.username
// }
// };

// logger.debug("[shell] %cSession is ready%c:", "color: white; background-color: green;", "", session);

// sessionManager.setSession(session);
// });

// const subscriptionPromise = axios.get("/api/subscription")
// .then(({ data }) => {
// const _subscription: Subscription = {
// company: data.company,
// contact: data.contact,
// status: data.status
// };

// logger.debug("[shell] %cSubscription is ready%c:", "color: white; background-color: green;", "", _subscription);

// setSubscription(_subscription);
// });

// Promise.all([sessionPromise, subscriptionPromise])
// .catch((error: unknown) => {
// if (axios.isAxiosError(error) && error.response?.status === 401) {
// // The authentication boundary will redirect to the login page.
// return;
// }

// throw error;
// })
// .finally(() => {
// setIsReady(true);
// });
// } else {
// logger.debug(`[shell] Passing through as "${window.location}" is a public route.`);

// setIsReady(true);
// }
// }
// }, [areModulesReady, isMswStarted, isActiveRouteProtected, logger, sessionManager]);

// const router = useMemo(() => {
// return createBrowserRouter([
// {
// element: <RootRoute />,
// children: routes
// }
// ]);
// }, [routes]);

// if (!isReady) {
// return <div>Loading...</div>;
// }

// return (
// <TelemetryServiceContext.Provider value={telemetryService}>
// <SubscriptionContext.Provider value={subscription}>
// <RouterProvider router={router} />
// </SubscriptionContext.Provider>
// </TelemetryServiceContext.Provider>
// );
// }
Loading

0 comments on commit ba4fa3a

Please sign in to comment.