Search code examples
reactjsreact-router-dom

Using Outlet on my protected route is showing a black page


I'm working with React and updated my app to use an overall layout.

There are public routes and protected routes.

When I changed the code from using children to <Outlet> I'm seeing a black page on when I navigate to any of my protected routes.

I've debugged for a bit now and I cannot figure it out.

It wills how the "Loading Protected Route"... really quick and then goes black.

Any ideas what it could be?

My understanding is I need to use <Outlet> instead of children

I even tried to return only return <Outlet/>; and it still shows black.

Here is my setup:

import Admin from "./restricted/Admin";

import { AuthProvider, Logout, APIProvider, ToastProvider, ErrorPage, ErrorBoundaryRouter, ProtectedRoute } from "@app/Shared"
import { Route, Routes } from "react-router-dom";

import HomePage from "./home/Home";
import Layout from "./layouts/Layout";

function App() {
  return (
    <ErrorBoundaryRouter fallbackUrl="/ErrorPage">
    <AuthProvider>
      <APIProvider>
        <ToastProvider>
        <Routes>
          {/* All routes share the Layout */}
          <Route element={<Layout />}>
              {/* Public Routes */}
              <Route path="/" element={<HomePage />} />
              <Route path="/public" element={<Public/>} />
            {/* Error Route */}
            <Route path="/Error/:message" element={<ErrorPage />} />
            
            {/* Protected Routes */}
            <Route element={<ProtectedRoute/>}>
              <Route path="/admin" element={<Admin />} />
              <Route path="/userUpdate" element={<userUpdate />} />
              <Route path="/logout" element={<Logout />} />
            </Route> 
          </Route>
        </Routes>
        </ToastProvider>
      </APIProvider>
    </AuthProvider>  
    </ErrorBoundaryRouter>  
  );
}
export default App;

Here is the Protected Route:

import { useEffect, useState } from "react";
import { useAuth } from "./useAuth";
import { userServer } from "../api/userServer";
import { Outlet } from "react-router-dom";


export const ProtectedRoute = () => {
  const { user, fetchUserClaims } = useAuth();
  const userAPI = userServer();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const checkAuthentication = async () => {
      try {
        const isLoggedIn = await userAPI.getUserLoggedInStatus();
        if (!isLoggedIn) {
          window.location.href = "/"; // Redirect to home if not logged in
        } else if (!user) {
          await fetchUserClaims();
        }
      } catch (error) {
        console.error("Error during authentication check:", error);
        window.location.href = "/"; // Redirect to home on error
      } finally {
        setLoading(false);
      }
    };

    checkAuthentication();
  }, [user]);

  if (loading) {
    return <div>Loading Protected Content...</div>;
  }

  return user && user.isLoggedIn ? <Outlet/>: null;
};

export default ProtectedRoute;

Here is my Layout:

import React, { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { TopToolbar, useAuth, isAuthenticated } from "@app/shared";
import { CardItem, getCardItems } from "../home/cardItems";

const Layout: React.FC = () => {
  const { logout, fetchUserClaims } = useAuth();

  const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
  const [userData, setUserData] = useState<{ firstName: string; lastName: string } | null>(null);
  const [initials, setInitials] = useState<string>("?");
  const [cardItems, setCardItems] = useState<CardItem[]>([]);

  // Helper function to calculate initials
  const calculateInitials = (firstName?: string, lastName?: string) => {
    if (firstName && lastName) {
      return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
    }
    return "?";
  };

  // Fetch user data and determine card items
  const checkAuthentication = async () => {
    try {
      const loggedInStatus = await isAuthenticated();
      setIsLoggedIn(loggedInStatus);

      if (loggedInStatus) {
        const claims = await fetchUserClaims();
        if (claims) {
          setUserData({ firstName: claims.firstName, lastName: claims.lastName });
          setInitials(calculateInitials(claims.firstName, claims.lastName));
          setCardItems(getCardItems(true)); // Get logged-in card items
        }
      } else {
        setUserData(null);
        setInitials("?");
        setCardItems(getCardItems(false)); // Get guest card items
      }
    } catch (error) {
      setIsLoggedIn(false);
      setUserData(null);
      setInitials("?");
      setCardItems(getCardItems(false)); // Fallback for unauthenticated state
    }
  };

  useEffect(() => {
    checkAuthentication();
  }, []);

  const handleLogout = () => {
    logout();
    setIsLoggedIn(false);
    setUserData(null);
    setInitials("?");
    setCardItems(getCardItems(false));
  };

  return (
    <div className="h-screen flex flex-col">
      {/* Top Toolbar */}
      <div>
        <TopToolbar
          userInitials={initials}
          firstName={userData?.firstName || "Guest"}
          lastName={userData?.lastName || ""}
          onLogout={isLoggedIn ? handleLogout : () => {}}
          allowToggleMenu={isLoggedIn}
        />
      </div>

      {/* Main Content */}
      <main className="flex-grow overflow-auto">
        {/* Provide cardItems via Outlet context */}
        <Outlet context={{ cardItems }} />
      </main>
    </div>
  );
};

export default Layout;

Here is main:

import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { BrowserRouter } from 'react-router-dom';
import { registerLicense } from '@syncfusion/ej2-base'

registerLicense(window._env_.VITE_SYNCFUSION_LICENSE_KEY)

ReactDOM.createRoot(document.getElementById('root')!).render(
    <BrowserRouter>
      <App />
    </BrowserRouter>
)

Consuming app package.json

"dependencies": {
    "@syncfusion/ej2": "~27.2.2",
    "@syncfusion/ej2-base": "~27.2.2",
    "@syncfusion/ej2-data": "~27.2.2",
    "@syncfusion/ej2-react-buttons": "~27.2.2",
    "@syncfusion/ej2-react-dropdowns": "~26.1.38",
    "@syncfusion/ej2-react-grids": "~27.2.2",
    "@syncfusion/ej2-react-inputs": "~27.2.2",
    "@syncfusion/ej2-react-layouts": "~27.2.2",
    "@syncfusion/ej2-react-navigations": "~27.2.2",
    "@syncfusion/ej2-react-notifications": "~27.2.2",
    "@syncfusion/ej2-react-pdfviewer": "~27.2.3",
    "@syncfusion/ej2-react-popups": "~27.2.2",
    "@syncfusion/ej2-react-querybuilder": "~27.2.2",
    "dotenv": "~16.4.5",
    "immer": "~10.0.4",
    "react": "~18.2.0",
    "react-dom": "~18.2.0",
    "react-router-dom": "~7.0.1",
    "remark-rehype": "^11.1.1",
    "use-immer": "~0.9.0",
    "uuid": "~9.0.1",
    "zustand": "~4.5.2"
  },
  "devDependencies": {
    "@types/node": "~20.12.7",
    "@types/react": "~18.2.79",
    "@types/react-dom": "~18.2.25",
    "@types/uuid": "~9.0.8",
    "@typescript-eslint/eslint-plugin": "~7.7.1",
    "@typescript-eslint/parser": "~7.7.1",
    "@vitejs/plugin-basic-ssl": "~1.1.0",
    "@vitejs/plugin-react": "~4.3.3",
    "@vitejs/plugin-react-swc": "~3.6.0",
    "autoprefixer": "~10.4.19",
    "concurrently": "~8.2.2",
    "eslint": "~8.57.0",
    "eslint-plugin-react-hooks": "~4.6.0",
    "eslint-plugin-react-refresh": "~0.4.6",
    "postcss": "~8.4.38",
    "tailwindcss": "~3.4.3",
    "typescript": "~5.4.5",
    "vite": "~5.2.10"
  }

Shared Lib dependencies

  "dependencies": {
    "@syncfusion/ej2-popups": "~27.2.2",
    "@syncfusion/ej2-react-buttons": "~27.2.2",
    "@syncfusion/ej2-react-dropdowns": "~27.2.2",
    "@syncfusion/ej2-react-notifications": "27.2.2",
    "@syncfusion/ej2-react-popups": "~27.2.2",
    "dompurify": "^3.2.1",
    "dotenv": "^16.4.5",
    "react-router-dom": "^7.0.1",
    "rehype-stringify": "^10.0.1",
    "remark": "^15.0.1",
    "remark-parse": "^11.0.0",
    "remark-rehype": "^11.1.1",
    "unified": "^11.0.5",
    "uuid": "~9.0.1"
  },
  "devDependencies": {
    "@types/node": "^22.9.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@types/react-router-dom": "^5.3.3",
    "@types/uuid": "~9.0.8",
    "@vitejs/plugin-react": "^4.3.3",
    "autoprefixer": "^10.4.19",
    "chokidar-cli": "^3.0.0",
    "cpx": "^1.5.0",
    "npm-run-all": "^4.1.5",
    "postcss": "^8.4.38",
    "rimraf": "^6.0.1",
    "tailwindcss": "^3.4.14",
    "typescript": "^5.6.3",
    "vite": "^5.4.11"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "~7.0.1",
    "tailwindcss": "^3.4.3"
  }

Solution

  • I believe I have it figured out:

    We use Vite for our Shared Library.

    In my vite config, I had set the rollupOptions for external for 'react-router-dom'

    After I did that, I was able to use my ProtectedRoute component in my consuming app and let it live in the shared library.

    Now I can use the same ProtectedRoute for other apps.

     rollupOptions: {
            external: ['react', 'react-dom', 'react-router-dom', 'tailwindcss'],
    

    Full code:

    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    import path from 'path';
    import { exec } from 'child_process';
    
    export default defineConfig(({ mode }) => {
      const isDevelopment = mode === 'development';
      console.log('Vite mode:', mode);
      console.log('isDevelopment:', isDevelopment);
      
      return {
        plugins: [
          react(),
          {
            name: 'copy-dts-after-build',
            buildEnd() {
              if (isDevelopment) {
                // This command will run after Vite finishes the build in development mode
                exec('npm run copy:types', (err, stdout, stderr) => {
                  if (err) {
                    console.error(`Error copying .d.ts files: ${stderr}`);
                  } else {
                    console.log(`.d.ts files copied successfully: ${stdout}`);
                  }
                });
              }
            },
          },
        ],
        build: {
          sourcemap: isDevelopment,  // Enable sourcemaps only during development
          minify: !isDevelopment,    // Turn off minification during development
          watch: isDevelopment
            ? {
                include: 'src/**',
              }
            : undefined,
          lib: {
            entry: path.resolve(__dirname, 'src/index.ts'),
            name: 'platform-react-shared-frontend',
            formats: ['es', 'cjs'],
            fileName: (format) => `platform-react-shared-frontend.${format}.js`,
          },
          outDir: 'dist',
          emptyOutDir: false,
          rollupOptions: {
            external: ['react', 'react-dom', 'react-router-dom', 'tailwindcss'],
            output: {
              dir: 'dist',
            },
          },
        },
      };
    });