Eine React-App ohne durchdachte Layout-Struktur wird schnell chaotisch. Jede Route dupliziert Header, Sidebar und Navigation. CSS-Grid-Definitionen wiederholen sich. Responsive Breakpoints sind inkonsistent. Menü-State wird in zehn verschiedenen Komponenten verwaltet. Das Ergebnis: Wartungshölle.
Professionelle Apps trennen Layout von Content. Ein Layout definiert
die grundlegende Struktur (Header oben, Sidebar links, Content in der
Mitte), während der Content variabel ist. React Router’s
<Outlet /> macht diese Trennung elegant – Layouts
werden zu wiederverwendbaren Containern für verschiedene
Inhaltsseiten.
Typischer Ansatz ohne Layout-Abstraktion:
// ❌ Dashboard.tsx - Alles in einer Komponente
function Dashboard() {
return (
<div>
<header>
<Logo />
<Navigation />
<UserMenu />
</header>
<aside>
<Sidebar />
</aside>
<main>
<h1>Dashboard</h1>
{/* Dashboard content */}
</main>
</div>
);
}
// ❌ Settings.tsx - Gleiche Struktur, duplizierter Code
function Settings() {
return (
<div>
<header>
<Logo />
<Navigation />
<UserMenu />
</header>
<aside>
<Sidebar />
</aside>
<main>
<h1>Settings</h1>
{/* Settings content */}
</main>
</div>
);
}
Probleme: - Header/Sidebar-Code ist 10x dupliziert - Änderungen müssen in jeder Datei gemacht werden - Inkonsistenzen schleichen sich ein - State (offene Sidebar, aktives Menü) muss überall synchronisiert werden
Ein Layout ist eine Komponente, die die Struktur definiert, aber den Content als Kinder akzeptiert.
// layouts/MainLayout.tsx
import { ReactNode } from 'react';
interface MainLayoutProps {
children: ReactNode;
}
export function MainLayout({ children }: MainLayoutProps) {
return (
<div className="main-layout">
<header className="header">
<Logo />
<Navigation />
<UserMenu />
</header>
<aside className="sidebar">
<Sidebar />
</aside>
<main className="content">
{children}
</main>
</div>
);
}
// pages/Dashboard.tsx - Nur noch Content
export function Dashboard() {
return (
<MainLayout>
<h1>Dashboard</h1>
{/* Dashboard-spezifischer Content */}
</MainLayout>
);
}
// pages/Settings.tsx - Nutzt dasselbe Layout
export function Settings() {
return (
<MainLayout>
<h1>Settings</h1>
{/* Settings-spezifischer Content */}
</MainLayout>
);
}
Problem gelöst: Header und Sidebar existieren einmal, Content variiert.
Noch besser: Layout als Route-Element mit
<Outlet />. Outlet ist ein Platzhalter, den React
Router mit der aktuellen Child-Route füllt.
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { MainLayout } from './layouts/MainLayout';
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
import { Profile } from './pages/Profile';
function App() {
return (
<BrowserRouter>
<Routes>
{/* Layout als Parent-Route */}
<Route element={<MainLayout />}>
{/* Child-Routes rendern in <Outlet /> */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// layouts/MainLayout.tsx
import { Outlet } from 'react-router-dom';
export function MainLayout() {
return (
<div className="main-layout">
<header className="header">
<Logo />
<Navigation />
<UserMenu />
</header>
<aside className="sidebar">
<Sidebar />
</aside>
<main className="content">
<Outlet /> {/* Child-Route rendert hier */}
</main>
</div>
);
}
// pages/Dashboard.tsx - Kein Layout-Wrapper mehr nötig!
export function Dashboard() {
return (
<>
<h1>Dashboard</h1>
{/* Content */}
</>
);
}
React Router ersetzt <Outlet /> automatisch mit
der passenden Child-Komponente. Bei /dashboard →
<Dashboard />, bei /settings →
<Settings />.
Verschiedene App-Bereiche brauchen verschiedene Layouts.
AuthLayout: Kein Header/Sidebar, zentrierter Content
// layouts/AuthLayout.tsx
import { Outlet } from 'react-router-dom';
export function AuthLayout() {
return (
<div className="auth-layout">
<div className="auth-card">
<Logo />
<Outlet />
</div>
</div>
);
}
/* AuthLayout Styling */
.auth-layout {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.auth-card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}AdminLayout: Extended Sidebar mit Admin-Navigation
// layouts/AdminLayout.tsx
import { Outlet } from 'react-router-dom';
export function AdminLayout() {
return (
<div className="admin-layout">
<header className="header">
<Logo />
<h2>Admin Panel</h2>
<UserMenu />
</header>
<aside className="admin-sidebar">
<AdminNavigation />
</aside>
<main className="content">
<Outlet />
</main>
</div>
);
}
Route-Struktur mit verschiedenen Layouts:
// App.tsx
function App() {
return (
<BrowserRouter>
<Routes>
{/* Auth Routes - AuthLayout */}
<Route element={<AuthLayout />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
</Route>
{/* Main App Routes - MainLayout */}
<Route element={<MainLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/settings" element={<Settings />} />
</Route>
{/* Admin Routes - AdminLayout */}
<Route element={<AdminLayout />}>
<Route path="/admin/users" element={<AdminUsers />} />
<Route path="/admin/analytics" element={<AdminAnalytics />} />
</Route>
</Routes>
</BrowserRouter>
);
}
| Layout | Routes | Charakteristik |
|---|---|---|
| AuthLayout | /login, /register |
Zentriert, kein Header/Sidebar |
| MainLayout | /dashboard, /products |
Standard App-Layout |
| AdminLayout | /admin/* |
Extended Sidebar, Admin-Branding |
CSS Grid ist ideal für Layouts mit definierten Bereichen.
// layouts/MainLayout.tsx
export function MainLayout() {
return (
<div className="grid-layout">
<header className="header">Header</header>
<aside className="sidebar">Sidebar</aside>
<main className="content">
<Outlet />
</main>
<footer className="footer">Footer</footer>
</div>
);
}
.grid-layout {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 60px 1fr 60px;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
min-height: 100vh;
}
.header {
grid-area: header;
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar {
grid-area: sidebar;
background: #f9fafb;
border-right: 1px solid #e5e7eb;
padding: 1rem;
overflow-y: auto;
}
.content {
grid-area: content;
padding: 2rem;
overflow-y: auto;
}
.footer {
grid-area: footer;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
padding: 0 1rem;
display: flex;
align-items: center;
}
/* Responsive: Sidebar ausblenden auf Mobile */
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"content"
"footer";
}
.sidebar {
display: none;
}
}Grid-Vorteile: - Klare Struktur durch benannte Areas - Einfaches Responsive-Verhalten - Perfekt für fixed-size Headers/Sidebars
Flexbox für flexiblere Layouts:
export function FlexLayout() {
return (
<div className="flex-layout">
<header className="header">Header</header>
<div className="body">
<aside className="sidebar">Sidebar</aside>
<main className="content">
<Outlet />
</main>
</div>
<footer className="footer">Footer</footer>
</div>
);
}
.flex-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
height: 60px;
background: white;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
}
.body {
display: flex;
flex: 1;
}
.sidebar {
width: 250px;
background: #f9fafb;
border-right: 1px solid #e5e7eb;
flex-shrink: 0;
overflow-y: auto;
}
.content {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
.footer {
height: 60px;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
flex-shrink: 0;
}Flexbox-Vorteile: - Natürliches Verhalten bei dynamischen Inhalten - Bessere Kontrolle über Overflow - Einfacher für vertikale Layouts
Layouts brauchen oft State: Sidebar offen/geschlossen, aktives Menü-Item, User-Dropdown.
// contexts/LayoutContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface LayoutContextType {
sidebarOpen: boolean;
toggleSidebar: () => void;
closeSidebar: () => void;
openSidebar: () => void;
}
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
export function LayoutProvider({ children }: { children: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const toggleSidebar = () => setSidebarOpen(prev => !prev);
const closeSidebar = () => setSidebarOpen(false);
const openSidebar = () => setSidebarOpen(true);
return (
<LayoutContext.Provider value={{
sidebarOpen,
toggleSidebar,
closeSidebar,
openSidebar
}}>
{children}
</LayoutContext.Provider>
);
}
export function useLayout() {
const context = useContext(LayoutContext);
if (!context) {
throw new Error('useLayout must be used within LayoutProvider');
}
return context;
}
// layouts/MainLayout.tsx
import { Outlet } from 'react-router-dom';
import { useLayout } from '../contexts/LayoutContext';
import { Menu } from 'lucide-react';
export function MainLayout() {
const { sidebarOpen, toggleSidebar } = useLayout();
return (
<div className="main-layout">
<header className="header">
<button onClick={toggleSidebar} className="menu-button">
<Menu />
</button>
<Logo />
<UserMenu />
</header>
<div className="body">
{sidebarOpen && (
<aside className="sidebar">
<Navigation />
</aside>
)}
<main className="content">
<Outlet />
</main>
</div>
</div>
);
}
// App.tsx
import { LayoutProvider } from './contexts/LayoutContext';
function App() {
return (
<BrowserRouter>
<LayoutProvider>
<Routes>
<Route element={<MainLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
</LayoutProvider>
</BrowserRouter>
);
}
Desktop: Sidebar immer sichtbar
Mobile: Sidebar als Overlay, schließt nach Navigation
// layouts/MainLayout.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useLayout } from '../contexts/LayoutContext';
import { useMediaQuery } from '../hooks/useMediaQuery';
export function MainLayout() {
const { sidebarOpen, toggleSidebar, closeSidebar } = useLayout();
const location = useLocation();
const isMobile = useMediaQuery('(max-width: 768px)');
// Sidebar schließen bei Route-Wechsel auf Mobile
useEffect(() => {
if (isMobile) {
closeSidebar();
}
}, [location.pathname, isMobile]);
return (
<div className={`main-layout ${sidebarOpen ? 'sidebar-open' : ''}`}>
<header className="header">
<button onClick={toggleSidebar}>
<Menu />
</button>
<Logo />
<UserMenu />
</header>
<div className="body">
<aside className={`sidebar ${isMobile ? 'mobile' : 'desktop'}`}>
<Navigation />
</aside>
{/* Backdrop auf Mobile wenn Sidebar offen */}
{isMobile && sidebarOpen && (
<div className="backdrop" onClick={closeSidebar} />
)}
<main className="content">
<Outlet />
</main>
</div>
</div>
);
}
/* Desktop: Sidebar statisch */
.sidebar.desktop {
position: static;
width: 250px;
}
/* Mobile: Sidebar als Overlay */
.sidebar.mobile {
position: fixed;
top: 60px;
left: 0;
bottom: 0;
width: 250px;
background: white;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 100;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
.sidebar-open .sidebar.mobile {
transform: translateX(0);
}
.backdrop {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
}// hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
Sidebar-Navigation sollte anzeigen, welche Route aktiv ist.
// components/Navigation.tsx
import { NavLink } from 'react-router-dom';
import { Home, Settings, Users, BarChart } from 'lucide-react';
export function Navigation() {
return (
<nav className="nav">
<NavLink to="/dashboard" className="nav-item">
<Home />
<span>Dashboard</span>
</NavLink>
<NavLink to="/users" className="nav-item">
<Users />
<span>Users</span>
</NavLink>
<NavLink to="/analytics" className="nav-item">
<BarChart />
<span>Analytics</span>
</NavLink>
<NavLink to="/settings" className="nav-item">
<Settings />
<span>Settings</span>
</NavLink>
</nav>
);
}
.nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 6px;
color: #6b7280;
text-decoration: none;
transition: all 0.2s;
}
.nav-item:hover {
background: #f3f4f6;
color: #111827;
}
/* React Router fügt .active automatisch hinzu */
.nav-item.active {
background: #3b82f6;
color: white;
}
.nav-item svg {
width: 20px;
height: 20px;
}NavLink fügt automatisch .active CSS-Klasse
hinzu, wenn die Route matched.
Für komplexere Active-Logic:
<NavLink
to="/users"
className={({ isActive }) =>
isActive ? 'nav-item active' : 'nav-item'
}
>
Users
</NavLink>
// Oder mit Custom Logic
<NavLink
to="/users"
className={({ isActive, isPending }) => {
if (isPending) return 'nav-item loading';
if (isActive) return 'nav-item active';
return 'nav-item';
}}
>
Users
</NavLink>
Manche Bereiche brauchen eigene Sub-Layouts.
// App.tsx
<Routes>
<Route element={<MainLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
{/* Settings mit eigenem Sub-Layout */}
<Route path="/settings" element={<SettingsLayout />}>
<Route index element={<Navigate to="profile" replace />} />
<Route path="profile" element={<ProfileSettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="notifications" element={<NotificationSettings />} />
</Route>
</Route>
</Routes>
// layouts/SettingsLayout.tsx
import { Outlet, NavLink } from 'react-router-dom';
export function SettingsLayout() {
return (
<div className="settings-layout">
<h1>Settings</h1>
<div className="settings-container">
<nav className="settings-nav">
<NavLink to="profile">Profile</NavLink>
<NavLink to="security">Security</NavLink>
<NavLink to="notifications">Notifications</NavLink>
</nav>
<div className="settings-content">
<Outlet />
</div>
</div>
</div>
);
}
.settings-layout {
max-width: 1200px;
margin: 0 auto;
}
.settings-container {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
margin-top: 2rem;
}
.settings-nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.settings-nav a {
padding: 0.5rem 1rem;
border-radius: 4px;
color: #6b7280;
text-decoration: none;
}
.settings-nav a:hover {
background: #f3f4f6;
}
.settings-nav a.active {
background: #eff6ff;
color: #3b82f6;
font-weight: 500;
}
.settings-content {
background: white;
padding: 2rem;
border-radius: 8px;
border: 1px solid #e5e7eb;
}Hierarchie:
MainLayout (Header + Sidebar)
└─ SettingsLayout (Settings Navigation)
├─ ProfileSettings
├─ SecuritySettings
└─ NotificationSettings
// components/Breadcrumbs.tsx
import { useMatches, Link } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
interface RouteHandle {
crumb?: (data?: any) => string;
}
export function Breadcrumbs() {
const matches = useMatches();
const crumbs = matches
.filter(match => (match.handle as RouteHandle)?.crumb)
.map(match => ({
label: (match.handle as RouteHandle).crumb!(match.data),
path: match.pathname
}));
return (
<nav className="breadcrumbs">
{crumbs.map((crumb, index) => (
<span key={crumb.path}>
{index > 0 && <ChevronRight className="separator" />}
{index === crumbs.length - 1 ? (
<span className="current">{crumb.label}</span>
) : (
<Link to={crumb.path}>{crumb.label}</Link>
)}
</span>
))}
</nav>
);
}
// App.tsx mit handle-Prop
<Routes>
<Route
element={<MainLayout />}
handle={{ crumb: () => 'Home' }}
>
<Route
path="/products"
element={<Products />}
handle={{ crumb: () => 'Products' }}
/>
<Route
path="/products/:id"
element={<ProductDetail />}
handle={{ crumb: (data) => data?.product?.name || 'Loading...' }}
/>
</Route>
</Routes>
// layouts/MainLayout.tsx
import { Breadcrumbs } from '../components/Breadcrumbs';
export function MainLayout() {
return (
<div className="main-layout">
<header className="header">
{/* ... */}
</header>
<div className="body">
<aside className="sidebar">
<Navigation />
</aside>
<main className="content">
<Breadcrumbs />
<Outlet />
</main>
</div>
</div>
);
}
Layout kann Auth-Check durchführen und redirecten.
// layouts/ProtectedLayout.tsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function ProtectedLayout() {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <LoadingScreen />;
}
if (!user) {
// Redirect zu Login, merke aktuelle Location
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <MainLayout />;
}
// App.tsx
<Routes>
<Route element={<AuthLayout />}>
<Route path="/login" element={<Login />} />
</Route>
{/* Protected Routes */}
<Route element={<ProtectedLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
// layouts/RoleBasedLayout.tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { MainLayout } from './MainLayout';
import { AdminLayout } from './AdminLayout';
export function RoleBasedLayout() {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
// Admin bekommt AdminLayout
if (user.role === 'admin') {
return <AdminLayout />;
}
// Regular User bekommt MainLayout
return <MainLayout />;
}
// App.tsx
<Route element={<RoleBasedLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
</Route>
Fehler 1: Layout-State in jeder Komponente
// ❌ Falsch: Sidebar-State in jeder Page-Komponente
function Dashboard() {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<Layout sidebarOpen={sidebarOpen}>
{/* ... */}
</Layout>
);
}
// ✓ Richtig: State im Layout selbst oder Context
function MainLayout() {
const [sidebarOpen, setSidebarOpen] = useState(true);
// ...
}
Fehler 2: Outlet vergessen
// ❌ Falsch: Kein Outlet in Layout
function MainLayout() {
return (
<div>
<Header />
<Sidebar />
<main>
{/* Wo soll Child-Route rendern? */}
</main>
</div>
);
}
// ✓ Richtig: Outlet als Placeholder
function MainLayout() {
return (
<div>
<Header />
<Sidebar />
<main>
<Outlet />
</main>
</div>
);
}
Fehler 3: Fixed Heights ohne Overflow
/* ❌ Falsch: Content kann nicht scrollen */
.content {
height: calc(100vh - 60px);
}
/* ✓ Richtig: Overflow aktivieren */
.content {
height: calc(100vh - 60px);
overflow-y: auto;
}Fehler 4: Zu viele Layout-Varianten
// ❌ Falsch: 10 verschiedene Layouts für minimale Unterschiede
<DashboardLayout />
<DashboardWithChartsLayout />
<DashboardWideLayout />
<DashboardNoSidebarLayout />
// ✓ Richtig: Ein flexibles Layout mit Props
<MainLayout
sidebar={<DashboardSidebar />}
fullWidth={false}
/>
Fehler 5: Mobile-Responsive als Afterthought
/* ❌ Falsch: Sidebar bricht Layout auf Mobile */
.sidebar {
width: 250px;
/* Was passiert bei 320px Viewport? */
}
/* ✓ Richtig: Mobile-First oder klare Breakpoints */
.sidebar {
width: 100%;
}
@media (min-width: 768px) {
.sidebar {
width: 250px;
}
}| Element | Zweck | Technologie |
|---|---|---|
| Layout-Komponente | Struktur-Container | React Component |
<Outlet /> |
Route-Content-Placeholder | React Router |
| Layout Context | Shared State (Sidebar, etc.) | Context API |
| CSS Grid/Flexbox | Visuelle Anordnung | CSS |
<NavLink> |
Navigation mit Active-State | React Router |
| Breadcrumbs | Hierarchie-Navigation | React Router Matches |
| Protected Routes | Auth-Check | React Router + Context |
Layouts sind das Fundament jeder professionellen React-App. Ein gut strukturiertes Layout-System spart nicht nur Entwicklungszeit – es macht die App wartbar, konsistent und skalierbar. Die Investment in durchdachte Layout-Komponenten zahlt sich aus, sobald die dritte Route hinzukommt.