Appearance
Kapitel 16: Projekt - Blog-System
In diesem Kapitel bauen wir ein Blog-System mit React Router und Axios.
16.1 Projektinitialisierung
📦 Pakete installieren
bash
# React Router
pnpm add react-router-dom
# Axios
pnpm add axios
# UUID (für ID-Generierung)
pnpm add uuid
pnpm add -D @types/uuid📂 Projektstruktur
src/
├── App.jsx
├── main.jsx
├── components/
│ ├── Header.jsx
│ ├── Footer.jsx
│ └── BlogCard.jsx
├── pages/
│ ├── Home.jsx
│ ├── BlogDetail.jsx
│ ├── CreateBlog.jsx
│ └── Login.jsx
├── hooks/
│ └── useFetch.js
├── api/
│ └── blogApi.js
└── App.css16.2 Routing konfigurieren
🌐 App.jsx (Router-Konfiguration)
jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import BlogDetail from './pages/BlogDetail';
import CreateBlog from './pages/CreateBlog';
import Login from './pages/Login';
function App() {
return (
<BrowserRouter>
<div className="app">
<Header />
<main className="main-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/:id" element={<BlogDetail />} />
<Route path="/create" element={<CreateBlog />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
<Footer />
</div>
</BrowserRouter>
);
}
export default App;16.3 API-Modul erstellen
🔧 blogApi.js (Axios-Instanz)
javascript
import axios from 'axios';
const blogApi = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request Interceptor
blogApi.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Response Interceptor
blogApi.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default blogApi;16.4 Custom Hook: useFetch
🔄 hooks/useFetch.js
javascript
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;16.5 Seitenlayout
1️⃣ Header.jsx
jsx
import { Link, NavLink } from 'react-router-dom';
function Header() {
return (
<header className="header">
<div className="header-content">
<Link to="/" className="logo">
Mein Blog
</Link>
<nav className="nav">
<NavLink to="/" end>
Startseite
</NavLink>
<NavLink to="/create">
Neuen Blog erstellen
</NavLink>
<NavLink to="/login">
Login
</NavLink>
</nav>
</div>
</header>
);
}
export default Header;2️⃣ Footer.jsx
jsx
function Footer() {
return (
<footer className="footer">
<div className="footer-content">
<p>© 2024 Mein Blog. Alle Rechte vorbehalten.</p>
</div>
</footer>
);
}
export default Footer;3️⃣ BlogCard.jsx
jsx
import { Link } from 'react-router-dom';
function BlogCard({ blog }) {
return (
<div className="blog-card">
<h3 className="blog-title">
<Link to={`/blog/${blog.id}`}>{blog.title}</Link>
</h3>
<p className="blog-body">{blog.body.substring(0, 100)}...</p>
<div className="blog-meta">
<span>Autor: User {blog.userId}</span>
</div>
</div>
);
}
export default BlogCard;16.6 Seiten implementieren
1️⃣ Home.jsx (Blog-Liste)
jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
import BlogCard from '../components/BlogCard';
import './Home.css';
function Home() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setPosts(response.data.slice(0, 10)); // Nur erste 10 Posts
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) return <div className="loading">Lädt...</div>;
if (error) return <div className="error">Fehler: {error}</div>;
return (
<div className="home">
<h1>Alle Blogs</h1>
<div className="blog-list">
{posts.map(post => (
<BlogCard key={post.id} blog={post} />
))}
</div>
</div>
);
}
export default Home;2️⃣ BlogDetail.jsx (Blog-Details)
jsx
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';
import axios from 'axios';
import './BlogDetail.css';
function BlogDetail() {
const { id } = useParams();
const navigate = useNavigate();
const [post, setPost] = useState(null);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [postRes, commentsRes] = await Promise.all([
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`),
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`)
]);
setPost(postRes.data);
setComments(commentsRes.data);
} catch (error) {
console.error('Fehler:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, [id]);
if (loading) return <div>Lädt...</div>;
if (!post) return <div>Blog nicht gefunden</div>;
return (
<div className="blog-detail">
<button onClick={() => navigate(-1)} className="back-button">
← Zurück
</button>
<h1>{post.title}</h1>
<p className="blog-content">{post.body}</p>
<div className="comments-section">
<h2>Kommentare ({comments.length})</h2>
{comments.map(comment => (
<div key={comment.id} className="comment">
<h4>{comment.name}</h4>
<p>{comment.body}</p>
</div>
))}
</div>
</div>
);
}
export default BlogDetail;3️⃣ CreateBlog.jsx (Blog erstellen)
jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import './CreateBlog.css';
function CreateBlog() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
title: '',
body: '',
userId: 1
});
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', formData);
alert('Blog erfolgreich erstellt!');
navigate(`/blog/${response.data.id}`);
} catch (error) {
alert('Fehler beim Erstellen des Blogs');
} finally {
setLoading(false);
}
};
return (
<div className="create-blog">
<h1>Neuen Blog erstellen</h1>
<form onSubmit={handleSubmit} className="blog-form">
<div className="form-group">
<label htmlFor="title">Titel</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="body">Inhalt</label>
<textarea
id="body"
name="body"
value={formData.body}
onChange={handleChange}
rows="10"
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Wird gespeichert...' : 'Blog veröffentlichen'}
</button>
</form>
</div>
);
}
export default CreateBlog;4️⃣ Login.jsx (Login-Seite)
jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './Login.css';
function Login() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
// Simuliere Login
if (formData.email && formData.password) {
localStorage.setItem('token', 'fake-jwt-token');
localStorage.setItem('user', JSON.stringify({ email: formData.email }));
alert('Login erfolgreich!');
navigate('/');
} else {
alert('Bitte alle Felder ausfüllen');
}
};
return (
<div className="login">
<h1>Login</h1>
<form onSubmit={handleSubmit} className="login-form">
<div className="form-group">
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Passwort</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<button type="submit">Einloggen</button>
</form>
</div>
);
}
export default Login;16.7 Stile hinzufügen
🎨 App.css (Hauptstiele)
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
width: 100%;
}
/* Header */
.header {
background-color: #282c34;
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
}
.nav {
display: flex;
gap: 20px;
}
.nav a {
color: white;
text-decoration: none;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.2s;
}
.nav a:hover {
background-color: rgba(255,255,255,0.1);
}
.nav a.active {
background-color: rgba(255,255,255,0.2);
}
/* Footer */
.footer {
background-color: #282c34;
color: white;
padding: 1rem 0;
text-align: center;
margin-top: auto;
}
/* Blog Card */
.blog-card {
background-color: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.blog-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.blog-title a {
color: #282c34;
text-decoration: none;
}
.blog-title a:hover {
color: #007bff;
}
.blog-body {
color: #666;
margin: 10px 0;
}
.blog-meta {
font-size: 0.9rem;
color: #999;
}
/* Blog List */
.blog-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
/* Blog Detail */
.blog-detail {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.back-button {
background-color: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 20px;
}
.back-button:hover {
background-color: #5a6268;
}
.blog-content {
font-size: 1.1rem;
line-height: 1.8;
margin: 20px 0;
}
.comments-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.comment {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
}
/* Forms */
.blog-form,
.login-form {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 600px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
}
button[type="submit"] {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
width: 100%;
}
button[type="submit"]:hover {
background-color: #0056b3;
}
button[type="submit"]:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* Loading & Error */
.loading,
.error {
text-align: center;
padding: 40px;
font-size: 1.2rem;
}
.error {
color: #dc3545;
}16.8 Performance-Optimierung
⚡ memo für BlogCard
jsx
import { memo } from 'react';
const BlogCard = memo(function BlogCard({ blog }) {
return (
<div className="blog-card">
<h3>{blog.title}</h3>
<p>{blog.body.substring(0, 100)}...</p>
</div>
);
});
export default BlogCard;⚡ useMemo für gefilterte Liste
jsx
import { useMemo } from 'react';
function Home() {
const [posts, setPosts] = useState([]);
const [search, setSearch] = useState('');
const filteredPosts = useMemo(() => {
return posts.filter(post =>
post.title.toLowerCase().includes(search.toLowerCase())
);
}, [posts, search]);
return (
<div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Blogs durchsuchen..."
/>
<div className="blog-list">
{filteredPosts.map(post => (
<BlogCard key={post.id} blog={post} />
))}
</div>
</div>
);
}📝 Zusammenfassung
In diesem Kapitel haben wir gelernt:
- ✅ Blog-System Projekt zu initialisieren
- ✅ React Router für Navigation zu verwenden
- ✅ Axios für API-Aufrufe zu verwenden
- ✅ Custom Hooks zu erstellen (
useFetch) - ✅ Verschiedene Seiten zu implementieren (Home, Detail, Create, Login)
- ✅ Performance-Optimierung mit
memounduseMemo
🎯 Nächste Schritte
Im nächsten Kapitel werden wir lernen:
- Häufige Fehler und Fallstricke in React
- Debugging-Techniken
- Best Practices für Produktionscode
Bereit für Fehlerbehebung? → Kapitel 17: Häufige Fehler
