Global State Management mit React Context
In diesem Abschnitt werden wir lernen, wofür der React Context ist und diesen in der App implementieren.
Was ist React Context?
Abschnitt betitelt „Was ist React Context?“React Context ist eine Möglichkeit, Daten durch den Komponenten-Baum zu leiten, ohne Props manuell auf jeder Ebene weiterzureichen. Es ist besonders nützlich für:
- Globale Daten, die von vielen Komponenten benötigt werden (z.B. Benutzerinformationen, Theme)
- Funktionen, die an verschiedenen Stellen der App verwendet werden sollen
- Zustand, der über mehrere Komponenten hinweg geteilt werden muss
Context hilft, das “Prop Drilling” Problem zu vermeiden, bei dem Daten durch viele Komponenten-Ebenen gereicht werden müssen.
1. Erstellen eines FavoritesContext
Abschnitt betitelt „1. Erstellen eines FavoritesContext“Der FavoritesContext wird für die Verwaltung der Lieblingsbücher verwendet:
import { createContext, useState, useContext } from "react";
// Create context and custom hookexport const FavoritesContext = createContext();export const useFavorites = () => useContext(FavoritesContext);
export function FavoritesProvider({ children }) { // Simple state for favorites const [favorites, setFavorites] = useState([]);
// Add or remove from favorites const toggleFavorite = (book) => { setFavorites((prev) => prev.some((fav) => fav.id === book.id) ? prev.filter((fav) => fav.id !== book.id) : [...prev, book] ); };
// Check if a book is favorite const isFavorite = (bookId) => favorites.some((book) => book.id === bookId);
return ( <FavoritesContext.Provider value={{ favorites, toggleFavorite, isFavorite }} > {children} </FavoritesContext.Provider> );}2. AppRouter aktualisieren für den Context
Abschnitt betitelt „2. AppRouter aktualisieren für den Context“Wir müssen den FavoritesProvider in unserer App-Hierarchie einbinden:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";import { FavoritesProvider } from "./context/FavoritesContext";import App from "./App";import HomePage from "./pages/HomePage/HomePage";import SearchPage from "./pages/SearchPage/SearchPage";import BookDetailsPage from "./pages/BookDetailsPage/BookDetailsPage";import FavoritesPage from "./pages/FavoritesPage/FavoritesPage";import NotFoundPage from "./pages/NotFoundPage/NotFoundPage";
function AppRouter() { return ( <FavoritesProvider> <Router> <Routes> <Route path="/" element={<App />}> <Route index element={<HomePage />} /> <Route path="search" element={<SearchPage />} /> <Route path="book/:id" element={<BookDetailsPage />} /> <Route path="favorites" element={<FavoritesPage />} /> <Route path="*" element={<NotFoundPage />} /> </Route> </Routes> </Router> </FavoritesProvider> );}
export default AppRouter;3. BookCard mit Context für Favoriten-Funktion
Abschnitt betitelt „3. BookCard mit Context für Favoriten-Funktion“Aktualisieren der BookCard-Komponente, um die Favoriten-Funktionalität aus dem Context zu nutzen:
import { Card, Button } from "react-bootstrap";import { Link } from "react-router-dom";import { useFavorites } from "../../context/FavoritesContext";
function BookCard({ book }) { const { id, title, author, imageUrl, description } = book; const { isFavorite, toggleFavorite } = useFavorites();
const isBookFavorite = isFavorite(id);
const handleFavoriteClick = (e) => { e.preventDefault(); toggleFavorite(book); };
return ( <Card className="h-100"> <Link to={`/book/${id}`} className="text-decoration-none text-dark"> <div className="d-flex h-100"> <div style={{ width: "130px" }}> <Card.Img src={ imageUrl || "https://via.placeholder.com/130x200?text=Kein+Bild" } alt={`Cover von ${title}`} style={{ height: "200px", objectFit: "cover" }} /> </div> <Card.Body> <Card.Title>{title}</Card.Title> <Card.Subtitle className="mb-2 text-muted">{author}</Card.Subtitle> <Card.Text> {description ? description.length > 150 ? `${description.substring(0, 150)}...` : description : "Keine Beschreibung verfügbar"} </Card.Text> <span className="text-primary">Details anzeigen</span> </Card.Body> </div> </Link> <Card.Footer className="bg-white border-top-0 text-end"> <Button variant={isBookFavorite ? "danger" : "outline-danger"} size="sm" onClick={handleFavoriteClick} > {isBookFavorite ? "❤️" : "🖤"} </Button> </Card.Footer> </Card> );}
export default BookCard;4. FavoritesPage mit Context
Abschnitt betitelt „4. FavoritesPage mit Context“Aktualisieren der FavoritesPage, um die Favoriten aus dem Context anzuzeigen:
import { Container, Alert } from "react-bootstrap";import BookList from "../../components/BookList/BookList";import { useFavorites } from "../../context/FavoritesContext";
function FavoritesPage() { const { favorites } = useFavorites();
return ( <Container> <h1>Meine Favoriten</h1>
{favorites.length > 0 ? ( <> <Alert variant="success"> Sie haben {favorites.length}{" "} {favorites.length === 1 ? "Buch" : "Bücher"} in Ihren Favoriten </Alert> <BookList books={favorites} /> </> ) : ( <Alert variant="info"> <Alert.Heading>Keine Favoriten</Alert.Heading> <p> Sie haben noch keine Bücher zu den Favoriten hinzugefügt. Füge Bücher hinzu, indem Sie auf das Herz-Symbol auf der Buchkarte klicken. </p> </Alert> )} </Container> );}
export default FavoritesPage;5. BookDetailsPage mit Context
Abschnitt betitelt „5. BookDetailsPage mit Context“Die BookDetailsPage wird aktualisiert, um den Favoriten-Status zu nutzen:
import { useState, useEffect } from "react";import { useParams, Link } from "react-router-dom";import { Container, Row, Col, Button, Card, Spinner, Alert, Badge,} from "react-bootstrap";import { getGoogleBookById } from "../../services/googleBooksService";import { useFavorites } from "../../context/FavoritesContext";
function BookDetailsPage() { const { id } = useParams(); const [book, setBook] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { isFavorite, toggleFavorite } = useFavorites();
useEffect(() => { async function fetchBookDetails() { try { setLoading(true); const bookData = await getGoogleBookById(id); setBook(bookData); setError(null); } catch (err) { setError("Fehler beim Laden der Buchdetails."); console.error("Error fetching book details:", err); } finally { setLoading(false); } }
fetchBookDetails(); }, [id]);
if (loading) { return ( <Container className="text-center my-5"> <Spinner animation="border" /> <p>Buchdetails werden geladen...</p> </Container> ); }
if (error) { return <Alert variant="danger">{error}</Alert>; }
if (!book) { return <Alert variant="warning">Buch nicht gefunden.</Alert>; }
const isBookFavorite = isFavorite(book.id);
return ( <Container> <Link to="/search" className="btn btn-outline-primary mb-4"> ← Zurück zur Suche </Link>
<Card> <Card.Body> <Row> <Col md={4} className="text-center mb-4 mb-md-0"> <img src={ book.imageUrl || "https://via.placeholder.com/200x300?text=Kein+Bild" } alt={`Cover von ${book.title}`} className="img-fluid shadow-sm" style={{ maxHeight: "300px" }} /> <div className="mt-3"> <Button variant={isBookFavorite ? "danger" : "outline-danger"} className="w-100 mb-2" onClick={() => toggleFavorite(book)} > {isBookFavorite ? "Aus Favoriten entfernen" : "Zu Favoriten hinzufügen"} {isBookFavorite ? " ❤️" : " 🤍"} </Button>
{book.previewLink && ( <Button href={book.previewLink} target="_blank" variant="primary" className="w-100" > Vorschau bei Google Books </Button> )} </div> </Col>
<Col md={8}> <Card.Title as="h1">{book.title}</Card.Title> <Card.Subtitle className="mb-3 text-muted"> von {book.author} </Card.Subtitle>
{book.publishedDate && ( <p className="text-muted mb-2"> Veröffentlicht: {book.publishedDate} </p> )}
{book.pageCount && book.pageCount !== "Unbekannt" && ( <p className="text-muted mb-2">{book.pageCount} Seiten</p> )}
{book.categories && book.categories.length > 0 && ( <div className="mb-3"> <strong>Kategorien:</strong> <br /> {book.categories.map((category, index) => ( <Badge bg="secondary" className="me-1 mb-1" key={index}> {category} </Badge> ))} </div> )}
<Card.Text className="mt-4"> <h5>Beschreibung</h5> <div dangerouslySetInnerHTML={{ __html: book.description }} /> </Card.Text> </Col> </Row> </Card.Body> </Card> </Container> );}
export default BookDetailsPage;6. Header-Zähler für Favoriten
Abschnitt betitelt „6. Header-Zähler für Favoriten“Wir aktualisieren den Header, um die Anzahl der Favoriten anzuzeigen:
import { Navbar, Nav, Container, Badge } from "react-bootstrap";import { NavLink } from "react-router-dom";import { useFavorites } from "../../context/FavoritesContext";
function Header() { const { favorites } = useFavorites(); const favoritesCount = favorites.length;
return ( <Navbar bg="light" expand="lg"> <Container> <Navbar.Brand as={NavLink} to="/"> 📚 Bücher-Projekt </Navbar.Brand> <Navbar.Toggle aria-controls="basic-navbar-nav" /> <Navbar.Collapse id="basic-navbar-nav"> <Nav className="ms-auto"> <Nav.Link as={NavLink} to="/" end> Startseite </Nav.Link> <Nav.Link as={NavLink} to="/search"> Bücher suchen </Nav.Link> <Nav.Link as={NavLink} to="/favorites"> Favoriten {favoritesCount > 0 && ( <Badge bg="danger" pill className="ms-1"> {favoritesCount} </Badge> )} </Nav.Link> </Nav> </Navbar.Collapse> </Container> </Navbar> );}
export default Header;Zusammenfassung
Abschnitt betitelt „Zusammenfassung“Mit React Context haben wir einen globalen State für Favoriten implementiert. Die wichtigsten Punkte:
- Context erstellen: Der FavoritesContext ermöglicht den gemeinsamen Zugriff auf die Favoriten-Daten.
- Provider einrichten: Der FavoritesProvider umschliesst unsere App und verwaltet den State.
- Custom Hook: Der useFavorites-Hook vereinfacht den Zugriff auf den Context.
Durch diese Implementierung:
- Können wir Favoriten in jeder Komponente hinzufügen oder entfernen
- Sehen wir die Anzahl der Favoriten in der Navigation
- Haben eine dedizierte Seite für alle Favoriten
- Müssen keine Props durch mehrere Komponenten durchreichen
Dieser Ansatz ist für Einsteiger leicht verständlich und skaliert gut für mittelgrosse Anwendungen.