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
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.Full code:
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 :
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).