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