Zum Inhalt springen

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.

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 }
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.

Mit dem Provider sind alle Komponenten schlank:

TodoEintrag.tsx
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>
)
}
TodoFooter.tsx
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>
)
}
TodoFormular.tsx
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>
)
}
App.tsx
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.

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.

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.