React Router
若要將 Toolpad Core 整合到使用 React Router 的單頁應用程式(例如使用 Vite),請按照以下步驟操作。
將所有頁面包裝在 ReactRouterAppProvider
中
在您的路由器設定(例如 src/main.tsx
)中,使用共用的元件或元素(例如 src/App.tsx
)作為根版面配置路由,以使用來自 @toolpad/core/react-router
的 ReactRouterAppProvider
包裝整個應用程式。
您必須在此根版面配置元素或元件中使用來自 react-router
的 <Outlet />
元件。
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import App from './App';
import DashboardPage from './pages';
import OrdersPage from './pages/orders';
const router = createBrowserRouter([
{
Component: App, // root layout route
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
import * as React from 'react';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { ReactRouterAppProvider } from '@toolpad/core/react-router';
import { Outlet } from 'react-router';
import type { Navigation } from '@toolpad/core';
const NAVIGATION: Navigation = [
{
kind: 'header',
title: 'Main items',
},
{
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
];
const BRANDING = {
title: 'My Toolpad Core App',
};
export default function App() {
return (
<ReactRouterAppProvider navigation={NAVIGATION} branding={BRANDING}>
<Outlet />
</ReactRouterAppProvider>
);
}
建立儀表板版面配置
為您的儀表板頁面建立版面配置檔案(例如 src/layouts/dashboard.tsx
),也將其用作具有來自 react-router
的 <Outlet />
元件的版面配置路由
import * as React from 'react';
import { Outlet } from 'react-router';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import { PageContainer } from '@toolpad/core/PageContainer';
export default function Layout() {
return (
<DashboardLayout>
<PageContainer>
<Outlet />
</PageContainer>
</DashboardLayout>
);
}
DashboardLayout
元件為您的儀表板頁面提供一致的版面配置,包括側邊欄、導覽列和標頭。PageContainer
元件用於包裝頁面內容,並為導覽提供麵包屑。
然後,您可以將此版面配置元件新增到您的 React Router 設定(例如 src/main.tsx
),作為上面建立的根版面配置路由的子路由。
import Layout from './layouts/dashboard';
//...
const router = createBrowserRouter([
{
Component: App, // root layout route
children: [
{
path: '/',
Component: Layout,
},
],
},
]);
//...
建立頁面
建立儀表板頁面(例如 src/pages/index.tsx
)和訂單頁面(src/pages/orders.tsx
)。
import * as React from 'react';
import Typography from '@mui/material/Typography';
export default function DashboardPage() {
return <Typography>Welcome to Toolpad!</Typography>;
}
import * as React from 'react';
import Typography from '@mui/material/Typography';
export default function OrdersPage() {
return <Typography>Welcome to the Toolpad orders!</Typography>;
}
然後,您可以將這些頁面元件作為路由新增到您的 React Router 設定(例如 src/main.tsx
)。透過將它們新增為上面建立的版面配置路由的子路由,它們會自動使用該儀表板版面配置進行包裝
import DashboardPage from './pages';
import OrdersPage from './pages/orders';
//...
const router = createBrowserRouter([
{
Component: App, // root layout route
children: [
{
path: '/',
Component: Layout,
children: [
{
path: '',
Component: DashboardPage,
},
{
path: 'orders',
Component: OrdersPage,
},
],
},
],
},
]);
//...
完成了!您現在已將 Toolpad Core 整合到使用 React Router 的單頁應用程式中!
(選用)設定驗證
您可以使用 SignInPage
元件來新增驗證以及您選擇的外部驗證提供者。以下程式碼示範了使用 Firebase 設定驗證所需的程式碼。
定義 SessionContext
以作為模擬驗證提供者
import * as React from 'react';
export interface Session {
user: {
name?: string;
email?: string;
image?: string;
};
}
interface SessionContextType {
session: Session | null;
setSession: (session: Session) => void;
loading: boolean;
}
const SessionContext = React.createContext<SessionContextType>({
session: null,
setSession: () => {},
loading: true,
});
export default SessionContext;
export const useSession = () => React.useContext(SessionContext);
新增 Firebase 驗證
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const app = initializeApp({
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGE_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
});
export const firebaseAuth = getAuth(app);
export default app;
import {
GoogleAuthProvider,
GithubAuthProvider,
signInWithPopup,
setPersistence,
browserSessionPersistence,
signInWithEmailAndPassword,
signOut,
} from 'firebase/auth';
import { firebaseAuth } from './firebaseConfig';
const googleProvider = new GoogleAuthProvider();
const githubProvider = new GithubAuthProvider();
// Sign in with Google functionality
export const signInWithGoogle = async () => {
try {
return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => {
const result = await signInWithPopup(firebaseAuth, googleProvider);
return {
success: true,
user: result.user,
error: null,
};
});
} catch (error: any) {
return {
success: false,
user: null,
error: error.message,
};
}
};
// Sign in with GitHub functionality
export const signInWithGithub = async () => {
try {
return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => {
const result = await signInWithPopup(firebaseAuth, githubProvider);
return {
success: true,
user: result.user,
error: null,
};
});
} catch (error: any) {
return {
success: false,
user: null,
error: error.message,
};
}
};
// Sign in with email and password
export async function signInWithCredentials(email: string, password: string) {
try {
return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => {
const userCredential = await signInWithEmailAndPassword(
firebaseAuth,
email,
password,
);
return {
success: true,
user: userCredential.user,
error: null,
};
});
} catch (error: any) {
return {
success: false,
user: null,
error: error.message || 'Failed to sign in with email/password',
};
}
}
// Sign out functionality
export const firebaseSignOut = async () => {
try {
await signOut(firebaseAuth);
return { success: true };
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
};
// Auth state observer
export const onAuthStateChanged = (callback: (user: any) => void) => {
return firebaseAuth.onAuthStateChanged(callback);
};
將驗證和工作階段資料新增至 AppProvider
import * as React from 'react';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { ReactRouterAppProvider } from '@toolpad/core/react-router';
import { Outlet, useNavigate } from 'react-router';
import type { Navigation } from '@toolpad/core';
import {
firebaseSignOut,
signInWithGoogle,
onAuthStateChanged,
} from './firebase/auth';
import SessionContext, { type Session } from './SessionContext';
const NAVIGATION: Navigation = [
{
kind: 'header',
title: 'Main items',
},
{
title: 'Dashboard',
icon: <DashboardIcon />,
},
{
segment: 'orders',
title: 'Orders',
icon: <ShoppingCartIcon />,
},
];
const BRANDING = {
title: 'My Toolpad Core App',
};
const AUTHENTICATION: Authentication = {
signIn: signInWithGoogle,
signOut: firebaseSignOut,
};
export default function App() {
const [session, setSession] = React.useState<Session | null>(null);
const [loading, setLoading] = React.useState(true);
const sessionContextValue = React.useMemo(
() => ({
session,
setSession,
loading,
}),
[session, loading],
);
React.useEffect(() => {
// Returns an `unsubscribe` function to be called during teardown
const unsubscribe = onAuthStateChanged((user: User | null) => {
if (user) {
setSession({
user: {
name: user.displayName || '',
email: user.email || '',
image: user.photoURL || '',
},
});
} else {
setSession(null);
}
setLoading(false);
});
return () => unsubscribe();
}, []);
return (
<ReactRouterAppProvider
navigation={NAVIGATION}
branding={BRANDING}
session={session}
authentication={AUTHENTICATION}
>
<SessionContext.Provider value={sessionContextValue}>
<Outlet />
</SessionContext.Provider>
</ReactRouterAppProvider>
);
}
保護儀表板版面配置內的路由
import * as React from 'react';
import LinearProgress from '@mui/material/LinearProgress';
import { Outlet, Navigate, useLocation } from 'react-router';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
import { PageContainer } from '@toolpad/core/PageContainer';
import { useSession } from '../SessionContext';
export default function Layout() {
const { session, loading } = useSession();
const location = useLocation();
if (loading) {
return (
<div style={{ width: '100%' }}>
<LinearProgress />
</div>
);
}
if (!session) {
// Add the `callbackUrl` search parameter
const redirectTo = `/sign-in?callbackUrl=${encodeURIComponent(location.pathname)}`;
return <Navigate to={redirectTo} replace />;
}
return (
<DashboardLayout>
<PageContainer>
<Outlet />
</PageContainer>
</DashboardLayout>
);
}
您可以透過此機制保護任何頁面或頁面群組。
使用 SignInPage
元件建立登入頁面
'use client';
import * as React from 'react';
import { SignInPage } from '@toolpad/core/SignInPage';
import LinearProgress from '@mui/material/LinearProgress';
import { Navigate, useNavigate } from 'react-router';
import { useSession, type Session } from '../SessionContext';
import {
signInWithGoogle,
signInWithGithub,
signInWithCredentials,
} from '../firebase/auth';
export default function SignIn() {
const { session, setSession, loading } = useSession();
const navigate = useNavigate();
if (loading) {
return <LinearProgress />;
}
if (session) {
return <Navigate to="/" />;
}
return (
<SignInPage
providers={[
{ id: 'google', name: 'Google' },
{ id: 'github', name: 'GitHub' },
{ id: 'credentials', name: 'Credentials' },
]}
signIn={async (provider, formData, callbackUrl) => {
let result;
try {
if (provider.id === 'google') {
result = await signInWithGoogle();
}
if (provider.id === 'github') {
result = await signInWithGithub();
}
if (provider.id === 'credentials') {
const email = formData?.get('email') as string;
const password = formData?.get('password') as string;
if (!email || !password) {
return { error: 'Email and password are required' };
}
result = await signInWithCredentials(email, password);
}
if (result?.success && result?.user) {
// Convert Firebase user to Session format
const userSession: Session = {
user: {
name: result.user.displayName || '',
email: result.user.email || '',
image: result.user.photoURL || '',
},
};
setSession(userSession);
navigate(callbackUrl || '/', { replace: true });
return {};
}
return { error: result?.error || 'Failed to sign in' };
} catch (error) {
return {
error: error instanceof Error ? error.message : 'An error occurred',
};
}
}}
/>
);
}
將登入頁面新增至路由器
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router';
import App from './App';
import Layout from './layouts/dashboard';
import DashboardPage from './pages';
import OrdersPage from './pages/orders';
import SignInPage from './pages/signIn';
const router = createBrowserRouter([
{
Component: App,
children: [
{
path: '/',
Component: Layout,
children: [
{
path: '/',
Component: DashboardPage,
},
{
path: '/orders',
Component: OrdersPage,
},
],
},
{
path: '/sign-in',
Component: SignInPage,
},
],
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);