Context und Reducer
useReducer zentralisiert die State-Logik. useContext verteilt den State ohne Prop Drilling. Zusammen bilden sie ein schlankes, selbstgebautes State-Management — ohne externe Bibliothek.
Die Idee
Abschnitt betitelt „Die Idee“Der Reducer verwaltet den State und definiert alle erlaubten Übergänge. Der Context transportiert den State und dispatch in den Baum. Eine eigene Provider-Komponente kapselt beides — der Rest der App braucht nur noch einen Custom Hook.
TodoProvider ├── useReducer → todos + dispatch ├── Context.Provider → stellt beides bereit └── {children} ↓ TodoListe, TodoFooter, ... └── useTodos() → { todos, dispatch }Alles in einer Datei: TodoContext.tsx
Abschnitt betitelt „Alles in einer Datei: TodoContext.tsx“import { createContext, useContext, useReducer } from 'react'
// --- Typen ---
export type Todo = { id: string; title: string; completed: boolean }
export type TodoAction = | { type: 'ADD'; title: string } | { type: 'TOGGLE'; id: string } | { type: 'DELETE'; id: string } | { type: 'CLEAR_COMPLETED' }
// --- Reducer ---
function todoReducer(todos: Todo[], action: TodoAction): Todo[] { switch (action.type) { case 'ADD': return [...todos, { id: crypto.randomUUID(), title: action.title, completed: false }] case 'TOGGLE': return todos.map(t => t.id === action.id ? { ...t, completed: !t.completed } : t) case 'DELETE': return todos.filter(t => t.id !== action.id) case 'CLEAR_COMPLETED': return todos.filter(t => !t.completed) }}
// --- Context ---
type TodoContextType = { todos: Todo[] dispatch: (action: TodoAction) => void}
const TodoContext = createContext<TodoContextType | null>(null)
// --- Provider ---
export function TodoProvider({ children }: { children: React.ReactNode }) { const [todos, reactDispatch] = useReducer(todoReducer, [], () => { const gespeichert = localStorage.getItem('todos') return gespeichert ? JSON.parse(gespeichert) : [] })
function dispatch(action: TodoAction) { const neueTodos = todoReducer(todos, action) reactDispatch(action) localStorage.setItem('todos', JSON.stringify(neueTodos)) }
return ( <TodoContext.Provider value={{ todos, dispatch }}> {children} </TodoContext.Provider> )}
// --- Custom Hook ---
export function useTodos() { const ctx = useContext(TodoContext) if (!ctx) throw new Error('useTodos muss innerhalb von TodoProvider verwendet werden') return ctx}Der Custom Hook useTodos ist der einzige Export, den Komponenten brauchen — TodoContext selbst bleibt intern.
Die Komponenten
Abschnitt betitelt „Die Komponenten“Mit dem Provider sind alle Komponenten schlank:
import { useTodos, type Todo } from './TodoContext'
export function TodoEintrag({ todo }: { todo: Todo }) { const { dispatch } = useTodos()
return ( <li> <input type="checkbox" checked={todo.completed} onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })} /> <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.title} </span> <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>×</button> </li> )}import { useTodos } from './TodoContext'
export function TodoFooter() { const { todos, dispatch } = useTodos() const offene = todos.filter(t => !t.completed) const erledigte = todos.filter(t => t.completed)
return ( <footer> <span>{offene.length} offen</span> {erledigte.length > 0 && ( <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}> Erledigte löschen </button> )} </footer> )}import { useState } from 'react'import { useTodos } from './TodoContext'
export function TodoFormular() { const { dispatch } = useTodos() const [eingabe, setEingabe] = useState('')
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!eingabe.trim()) return dispatch({ type: 'ADD', title: eingabe.trim() }) setEingabe('') }
return ( <form onSubmit={handleSubmit}> <input value={eingabe} onChange={e => setEingabe(e.target.value)} placeholder="Neue Aufgabe..." /> <button type="submit">Hinzufügen</button> </form> )}import { TodoProvider } from './TodoContext'import { TodoFormular } from './TodoFormular'import { TodoEintrag } from './TodoEintrag'import { TodoFooter } from './TodoFooter'import { useTodos } from './TodoContext'
function TodoListe() { const { todos } = useTodos() return ( <ul> {todos.map(todo => <TodoEintrag key={todo.id} todo={todo} />)} </ul> )}
export default function App() { return ( <TodoProvider> <h1>Todos</h1> <TodoFormular /> <TodoListe /> <TodoFooter /> </TodoProvider> )}App hat keine Ahnung von todos oder dispatch — es setzt nur den Provider. Alle Logik und alle Daten kapselt TodoProvider.
TypeScript
Abschnitt betitelt „TypeScript“Der dispatch-Typ ändert sich durch das Wrapping.
useReducer gibt React.Dispatch<TodoAction> zurück — eine Funktion, die direkt an React dispatcht. Im Provider ersetzen wir sie durch unsere eigene Funktion, die zusätzlich in localStorage speichert. Deshalb deklarieren wir den Typ im Context als (action: TodoAction) => void statt React.Dispatch<TodoAction>:
type TodoContextType = { todos: Todo[] dispatch: (action: TodoAction) => void // unsere Wrapper-Funktion}Komponenten, die useTodos() verwenden, wissen dadurch nichts von React.Dispatch — sie rufen einfach dispatch({ type: 'ADD', title }) auf. Die Implementierung dahinter (ob direkt, gewrappt oder mit Middleware) ist ihr egal.
Typen zentral exportieren.
TodoContext.tsx ist die einzige Quelle der Wahrheit für Todo und TodoAction. Alle Komponenten importieren von dort:
import { useTodos, type Todo, type TodoAction } from './TodoContext'So entsteht kein geteilter Typ, der an mehreren Stellen parallel gepflegt werden muss.
Was dieses Muster kann — und was nicht
Abschnitt betitelt „Was dieses Muster kann — und was nicht“Dieses Muster ist gut genug für viele echte Anwendungen. Es ist einfach zu verstehen, braucht keine externe Abhängigkeit und ist vollständig in TypeScript typisiert.
Es hat aber Grenzen:
- Performance: Jede
dispatch-Aktion führt dazu, dass alle Konsumenten des Contexts neu rendern — auch wenn sich ihr Teil des States nicht geändert hat. Bei vielen Komponenten und häufigen Updates kann das spürbar werden. - DevTools: Externe Bibliotheken wie Redux oder Zustand bieten Browser-Erweiterungen, mit denen du State-Übergänge zeitlich nachverfolgen kannst. Das fehlt hier.
- Middleware: Logging, Persistenz, Undo/Redo — all das lässt sich hier manuell bauen, aber externe Libraries bieten es fertig.