skip to Main Content

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"
  }

2

Answers


  1. Chosen as BEST ANSWER

    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',
            },
          },
        },
      };
    });
    
    

  2. The issue you are facing (black screen) could arise if the loading state doesn’t resolve as expected or if the user is not correctly authenticated.

    so try to update your code like below :

    import { useEffect, useState } from "react";
    import { useAuth } from "./useAuth";
    import { userServer } from "../api/userServer";
    import { Outlet, Navigate } 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) {
              setLoading(false);  // Stop loading and redirect
            } else if (!user) {
              await fetchUserClaims();
            } else {
              setLoading(false);  // User data is loaded, stop loading
            }
          } catch (error) {
            console.error("Error during authentication check:", error);
            setLoading(false);  // Stop loading even on error
          }
        };
    
        checkAuthentication();
      }, [user, fetchUserClaims, userAPI]);
    
      if (loading) {
        return <div>Loading Protected Content...</div>;  // Loading state
      }
    
      if (!user || !user.isLoggedIn) {
        return <Navigate to="/" />;  // Redirect to home if not authenticated
      }
    
      return <Outlet />;  // Render protected content if authenticated
    };
    
    export default ProtectedRoute;
    

    Explanation : Instead of using window.location.href, we now use Navigate from react-router-dom to redirect the user when not authenticated or on error. This ensures a clean redirect without page reloads and works seamlessly with React Router.

    • The loading state is only set to false once the user authentication check is complete. This prevents rendering the Outlet before it’s ready.

    • If there is an error during the authentication check, we ensure that the loading state is updated and we stop further rendering or redirect accordingly.

    • If the user is not authenticated or the user.isLoggedIn is false, the user is redirected to the home page. Otherwise, it renders the Outlet (the protected route).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search