Es gibt ein Muster, das ich in fast jedem Team sehe, dem ich beitrete: Ein Entwickler entdeckt eine mächtige Abstraktion — Redux, MobX, XState, einen eigenen Event Bus — und installiert sie, «weil wir sie irgendwann brauchen werden.» Sechs Monate später pflegt das Team 200 Zeilen Boilerplate für State, der in einem useState-Aufruf leben könnte. Niemand erinnert sich, warum der Store existiert. Niemand traut sich, ihn zu entfernen.
Das ist das Gegenteil von gutem Engineering. Gutes Engineering ist die Wahl des einfachsten Werkzeugs, das das tatsächliche Problem löst. KISS — Keep It Simple, Stupid — ist kein nettes Akronym. Es ist das am meisten unterschätzte Prinzip in der Softwareentwicklung, besonders im React-Ökosystem, wo die Tool-Landschaft aktiv zum Over-Engineering ermutigt.
Die Cleverness-Falle
Der Dunning-Kruger-Effekt schlägt beim State Management am härtesten zu. Ein Entwickler, der gerade Redux gelernt hat, denkt, jedes Stück State gehöre in einen globalen Store. Er erstellt Action Types, Reducer, Selectors, Middleware — für einen Suchfilter, der von zwei Komponenten auf einer Seite verwendet wird. Es fühlt sich produktiv an. Es fühlt sich professionell an. Es ist nichts davon.
Cleverer Code ist eine Belastung. Jede Abstraktion, die du einführst, ist ein Konzept, das dein Team lernen, pflegen und debuggen muss. Redux fügt Actions, Reducer, Selectors, Middleware und eine Store-Konfigurationsschicht hinzu. Das sind fünf neue Konzepte für State Management — in einem Framework, das bereits eingebautes State Management hat. Du zahlst eine Komplexitätssteuer auf jedes Feature, das du baust, und die Rendite dieser Investition ist selten positiv.
Ich bin Projekten bei AXA und einer grossen Schweizer Bank beigetreten, wo das komplette Entfernen von Redux — Ersatz durch useState und useContext — den State-Management-Code um 70% reduzierte und Features schneller zu bauen machte. Nicht weil Redux schlechte Software ist. Weil es das falsche Werkzeug für das war, was diese Anwendungen tatsächlich brauchten.
// Redux approach for a simple filter + list UI
// store/filtersSlice.ts
const filtersSlice = createSlice({
name: 'filters',
initialState: { search: '', category: 'all' },
reducers: {
setSearch: (state, action) => { state.search = action.payload; },
setCategory: (state, action) => { state.category = action.payload; },
resetFilters: (state) => { state.search = ''; state.category = 'all'; },
},
});
// store/index.ts — configure store, combine reducers
// store/hooks.ts — typed useSelector, useDispatch
// components/Filters.tsx — useSelector + useDispatch
// components/List.tsx — useSelector to read filters
// 5 files, 80+ lines of boilerplate for two strings.
// The same thing with useState:
function ProductPage() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('all');
return (
<>
<Filters search={search} category={category}
onSearch={setSearch} onCategory={setCategory} />
<ProductList search={search} category={category} />
</>
);
}
// 1 file. 10 lines. Same result. Easier to read, easier to change.React State reicht (meistens)
React hat zwei eingebaute State-Primitive: useState für lokalen State und useContext für geteilten State. Zusammen decken sie die grosse Mehrheit realer Anwendungen ab. Bevor du nach einer externen Library greifst, frag dich: Kann ich das mit dem lösen, was React mir bereits gibt?
useState handhabt komponentenlokalen State — Formulareingaben, Toggles, Loading-Flags, ausgewählte Tabs. useContext handhabt State, den mehrere Komponenten brauchen — den aktuellen Benutzer, einen Warenkorb, eine Theme-Präferenz. Kombiniere sie und du hast eine State-Management-Lösung mit null Abhängigkeiten, null Boilerplate, und verstanden von jedem React-Entwickler auf dem Planeten.
Der Einwand, den ich höre, dreht sich immer um «Skalierung». Aber ich habe an grossen Anwendungen bei Vontobel und Migros gearbeitet — Echtzeit-Trading-Plattformen, Multi-Brand-E-Commerce — und Context + useState skalierten problemlos. Der Schlüssel ist, Provider strategisch zu platzieren: nicht ein riesiger Context an der Wurzel, sondern fokussierte Contexts für jede Domäne. Ein CartProvider umschliesst den Checkout-Flow. Ein ThemeProvider umschliesst die App. Ein DashboardProvider umschliesst die Dashboard-Seite. Jeder ist klein, fokussiert und einfach zu verstehen.
// When multiple components need the same data and
// props drilling goes beyond 3 levels — use Context.
interface CartContext {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: number;
}
const CartCtx = createContext<CartContext | null>(null);
function useCart() {
const ctx = useContext(CartCtx);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
}
// Smart component: owns the state, provides it via context
function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = (item: CartItem) =>
setItems((prev) => [...prev, item]);
const removeItem = (id: string) =>
setItems((prev) => prev.filter((i) => i.id !== id));
const total = items.reduce((sum, i) => sum + i.price, 0);
return (
<CartCtx.Provider value={{ items, addItem, removeItem, total }}>
{children}
</CartCtx.Provider>
);
}
// Any descendant can read cart state — no prop chains
function CartBadge() {
const { items } = useCart();
return <span>{items.length}</span>;
}
function CartTotal() {
const { total } = useCart();
return <span>{formatCurrency(total)}</span>;
}Der Komponentenbaum ist dein State Manager
Das knüpft direkt an die Smart- und Dumb-Component-Trennung an, über die ich bereits geschrieben habe. Der Komponentenbaum ist nicht nur eine Rendering-Hierarchie — er ist ein State-Verteilungssystem. Smart Components oben besitzen State und geben ihn weiter. Dumb Components darunter empfangen Daten und rendern sie.
Wenn du State am richtigen Ort platzierst — dem niedrigsten gemeinsamen Vorfahren der Komponenten, die ihn brauchen — brauchst du keinen Store. Der React-Baum übernimmt die Verteilung kostenlos. Eine DashboardPage-Komponente, die Daten abruft und sie an StatsGrid und ChartSection weitergibt, tut genau das, was Redux tun würde, aber ohne Zeremonie.
Das meine ich mit «der dümmste Code ist der klügste». Eine Smart Component mit useState und drei Props, die an Kinder weitergegeben werden, ist langweilig. Sie ist offensichtlich. Ein neuer Entwickler kann sie in 30 Sekunden lesen. Genau das willst du. Der Next.js-Warenkorb-Umbau, den ich in Vergiss Best Practices beschrieben habe, folgte demselben Prinzip — wir ersetzten serverseitig gerenderte Komplexität durch einfachen clientseitigen State, und alles wurde besser.
// The component tree IS your state architecture.
// Place state in the lowest common ancestor that needs it.
// ❌ Over-engineered: global store for page-local state
// Redux store → useSelector in Header, Sidebar, Content
// Now every component is coupled to a global store shape
// and re-renders on any store change unless you memoize everything.
// ✅ Simple: smart component owns the state, passes it down
function DashboardPage() {
const [period, setPeriod] = useState<'week' | 'month' | 'year'>('month');
const [data, setData] = useState<DashboardData | null>(null);
useEffect(() => {
fetchDashboard(period).then(setData);
}, [period]);
return (
<main>
{/* 1 level deep — just pass it */}
<PeriodSelector period={period} onChange={setPeriod} />
<StatsGrid data={data} />
<ChartSection data={data} period={period} />
</main>
);
}
// PeriodSelector, StatsGrid, ChartSection are all dumb.
// They receive data, render UI, report interactions.
// No store subscription. No selector. No dispatch.
// Change the data shape? Update one smart component.Props, Context oder beides
Ich setze eine einfache Regel in meinen Teams durch: Props Drilling ist bis zu 3 Ebenen tief in Ordnung. Darüber hinaus: Context verwenden. Das ist nicht willkürlich — es ist der Punkt, an dem das Durchreichen derselben Prop durch Zwischenkomponenten, die sie nicht verwenden, die Lesbarkeit beeinträchtigt und unnötige Kopplung erzeugt.
Props sind der Standard. Sie sind explizit, nachverfolgbar und typsicher. Wenn du die Props einer Komponente siehst, weisst du genau, was sie braucht. Diese Transparenz ist wertvoll — gib sie nicht für Bequemlichkeit auf.
Context ist für den Fall, dass Props Ketten erzeugen. Wenn eine Layout-Komponente eine user-Prop akzeptieren muss, nur um sie an eine Navigation-Komponente weiterzugeben, die sie an ein UserMenu weitergibt — das ist Rauschen. Keine dieser Zwischenkomponenten kümmert sich um den User. Context lässt dich die Kette überspringen und den Wert direkt dort lesen, wo er gebraucht wird.
Der Sweet Spot ist die Mischung aus beiden. Verwende Context für querschnittliche Belange, die viele Komponenten brauchen (User, Theme, Warenkorb, Locale). Verwende Props für Daten, die durch eine klare Eltern-Kind-Beziehung fliessen. Und verwende keines von beidem für State, der zu einer einzelnen Komponente gehört — dafür gibt es useState.
// Props drilling is fine — up to a point.
// My rule: max 3 levels deep. Beyond that, use Context.
// ✅ Fine — 2 levels
<Page>
<Section filters={filters}>
<FilterBar filters={filters} onChange={setFilters} />
</Section>
</Page>
// ❌ Too deep — 5 levels of threading the same prop
<Page>
<Layout user={user}>
<Sidebar user={user}>
<Navigation user={user}>
<UserMenu user={user}> {/* enough. */}
<Avatar user={user} />
</UserMenu>
</Navigation>
</Sidebar>
</Layout>
</Page>
// ✅ Context cuts the chain where it matters
<UserProvider user={user}>
<Layout>
<Sidebar>
<Navigation>
<UserMenu /> {/* useUser() — reads from context */}
</Navigation>
</Sidebar>
</Layout>
</UserProvider>
// The middle components (Layout, Sidebar, Navigation)
// no longer need to know about 'user' at all.
// They just render their children. Clean.Redundanz statt Abstraktion
Dieser Artikel ist der praktische Begleiter zu Warum YAGNI besser ist als DRY. Dasselbe Prinzip gilt für State Management: ein bisschen Redundanz ist günstiger als die falsche Abstraktion. Zwei Komponenten, die jeweils ihren eigenen lokalen State mit ähnlichen useState-Aufrufen verwalten, sind einfacher zu pflegen als zwei Komponenten, die sich einen globalen Store-Slice teilen, der entworfen wurde, um «Duplikation zu vermeiden».
Duplikation im State ist sichtbar und lokal. Du kannst sie sehen, und wenn die beiden States divergieren müssen (tun sie meistens), änderst du einfach einen, ohne den anderen anzufassen. Ein geteilter Store erzeugt unsichtbare Kopplung — ändere die Form für einen Consumer und du brichst den anderen. Ich habe Teams beobachtet, die ganze Sprints damit verbracht haben, geteilte Redux-Slices zu entwirren, die «Code sparten», aber Abhängigkeiten zwischen unzusammenhängenden Features erzeugten.
Die besten Teams, mit denen ich gearbeitet habe, setzen standardmässig auf einfach. Zuerst useState. Wenn mehrere Komponenten denselben State brauchen, hebe ihn zu einem gemeinsamen Elternteil hoch und reiche ihn weiter. Wenn die Prop-Kette zu tief wird, wickle ihn in einen Context. Wenn — und nur wenn — du wirklich Time-Travel-Debugging, komplexe State Machines oder optimistische Updates über viele Entitäten brauchst, dann ziehe eine dedizierte State-Library in Betracht. Aber die meisten Anwendungen kommen nie dahin. Und die, die es tun, brauchen es normalerweise in einem Teil der App, nicht überall.
Hör auf, Probleme zu lösen, die du nicht hast. Die einfachste Lösung, die funktioniert, ist die beste Lösung — und sie bleibt fast immer die beste Lösung, lange nachdem die clevere zu einem Wartungsalptraum geworden ist.

