Was ist Redux Toolkit?
Redux ist ein vorhersagbarer State Container: Ein einziger Store hält den Zustand Ihrer Anwendung, und die einzige Möglichkeit, diesen zu ändern, besteht darin, eine Action zu dispatchen, die von einem Pure Reducer verarbeitet wird. Genau diese Einschränkung macht Redux so leistungsfähig – jede Zustandsänderung ist explizit, nachvollziehbar und testbar.
Jahrelang war Redux zudem für seinen hohen Boilerplate-Anteil bekannt. Redux Toolkit hat das behoben. Es ist heute der offizielle und empfohlene Weg, Redux zu schreiben, und bündelt configureStore, createSlice, Immer für immutable Updates, thunk middleware und DevTools in einem Paket. Sie erhalten die Architektur ohne den unnötigen Overhead.
Der Store und Slices
Ein Slice bündelt die Reducer-Logik und die Actions eines Features an einem Ort. configureStore führt diese Slices im Store zusammen.
// app/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import todosReducer from "../features/todos/todosSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
},
});
// main.jsx
import { Provider } from "react-redux";
import { store } from "./app/store";
<Provider store={store}>
<App />
</Provider>;
createSlice generiert die Action Creator und den Reducer automatisch für dich, sodass du Action-Type-Konstanten nie händisch schreiben musst. Immer treibt die Reducer an, was bedeutet, dass du Code schreiben kannst, der wie eine Mutation aussieht, während der State unveränderlich (immutable) bleibt.
// features/todos/todosSlice.js
import { createSlice } from "@reduxjs/toolkit";
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
add(state, action) {
state.push(action.payload);
},
toggle(state, action) {
const todo = state.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
remove(state, action) {
return state.filter((t) => t.id !== action.payload);
},
},
});
export const { add, toggle, remove } = todosSlice.actions;
export default todosSlice.reducer;
Lesen und Dispatching
Komponenten lesen den State mit useSelector und senden Actions mit useDispatch.
// TodoList.jsx
import { useSelector, useDispatch } from "react-redux";
import { add, toggle } from "../features/todos/todosSlice";
export function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => dispatch(toggle(todo.id))}>
{todo.done ? "✓" : "○"} {todo.text}
</li>
))}
<button onClick={() => dispatch(add({ id: 1, text: "New" }))}>
Add
</button>
</ul>
);
}
Wähle den kleinstmöglichen Slice aus, den du benötigst. useSelector rendert die Komponente neu, wenn sich der ausgewählte Wert ändert; das Auswählen des gesamten States würde diese Optimierung zunichtemachen. Nutze createSelector von Reselect, wenn ein abgeleiteter Wert ansonsten bei jedem Render-Vorgang neu berechnet werden müsste.
Asynchrone Logik mit Thunks
createAsyncThunk generiert die drei Action-Typen, die ein asynchroner Request erzeugt und die du in extraReducers behandelst.
// features/users/usersSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUsers = createAsyncThunk("users/fetch", async () => {
const res = await fetch("/api/users");
if (!res.ok) throw new Error("Failed to fetch users");
return res.json();
});
const usersSlice = createSlice({
name: "users",
initialState: { items: [], status: "idle", error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
Dies ist das explizite Redux-Pattern: Loading, Success und Failure sind alles Actions, die du in den Devtools inspizieren kannst.
RTK Query für den Server-State
Beim Datenabruf macht RTK Query Thunks komplett überflüssig. Du definierst einen API-Slice mit Endpunkten, und das Tool übernimmt das Caching, die Deduplizierung, die Invalidierung sowie das Refetching.
// features/api/apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["Post"],
endpoints: (builder) => ({
getPosts: builder.query({ query: () => "posts", providesTags: ["Post"] }),
addPost: builder.mutation({
query: (body) => ({ url: "posts", method: "POST", body }),
invalidatesTags: ["Post"],
}),
}),
});
export const { useGetPostsQuery, useAddPostMutation } = api;
// Posts.jsx
const { data, isLoading, error } = useGetPostsQuery();
RTK Query ist die Antwort von Redux auf TanStack Query: Der Server-State wird in einem Cache mit eigenem Lebenszyklus verwaltet, während der Client-State in Slices bleibt. Wenn diese beiden Verantwortlichkeiten an einem Ort vermischt werden, wird der Code in Redux-Apps oft unübersichtlich.
Middleware und DevTools
configureStore aktiviert die Redux DevTools Extension und enthält standardmäßig die Thunk-middleware. Sie können eigene middleware für Logging, Analytics oder Side Effects hinzufügen.
// middleware.js
const logger = (store) => (next) => (action) => {
console.log("dispatching", action.type);
return next(action);
};
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefault) => getDefault().concat(logger),
});
Die DevTools ermöglichen es Ihnen, jede dispatched Action zu untersuchen, den State davor und danach zu vergleichen (Diff) und den Verlauf erneut abzuspielen – der wichtigste Grund, warum Teams Redux für komplexen State wählen.
Best Practices
- Verwende Redux Toolkit und niemals handgeschriebenes Redux.
- Erstelle pro Feature einen eigenen Slice und fasse zusammengehörige Logik zusammen.
- Wähle in
useSelectorden kleinstmöglichen State-Slice aus. - Nutze
createSelectorfür abgeleitete Daten, deren Berechnung rechenintensiv ist. - Verwalte Server-State in RTK Query, nicht in Slices.
- Speichere nur serialisierbare Daten; halte nicht-serialisierbare Werte aus dem Store fern.
- Modelliere Actions als Ereignisse, die bereits eingetreten sind, und nicht als Setter.
Häufige Fehler
- Den gesamten State auszuwählen und bei jeder Änderung ein Re-Rendering auszulösen.
- Serverdaten in Slices zu speichern und Lade-Flags manuell zu verwalten.
- Nicht-serialisierbare Werte wie Klasseninstanzen oder Funktionen zu speichern.
- Den State außerhalb eines Reducers zu mutieren.
- Redux für lokalen Component-State zu verwenden, den
useStatebesser handhaben würde. - Ausführliches Legacy Redux anstelle von Slices zu schreiben.
Wie geht es weiter?
Redux Toolkit ist die bewährte Wahl für große, komplexe Client-States. Vergleiche es mit der integrierten Context API von React sowie dem schlankeren Zustand und lagere Server-Belange in TanStack Query oder RTK Query aus. Für Vue wird diese Rolle von Pinia übernommen.