30 mapStateToProps und connect - Das klassische Redux Pattern

Die Funktion connect() aus der react-redux Bibliothek war jahrelang die Standard-Methode, um React-Komponenten mit dem Redux Store zu verbinden. Obwohl moderne React Hooks wie useSelector und useDispatch heute die bevorzugte Lösung darstellen, ist ein Verständnis von connect() und den zugehörigen Mapping-Funktionen essentiell für die Arbeit mit bestehenden Codebases und das Verständnis der Evolution von React-Redux.

Das connect() Pattern basiert auf dem Higher-Order Component (HOC) Ansatz und bietet eine deklarative Methode, um Store-Daten und Action-Creators als Props an Komponenten zu übergeben. Dabei werden zwei zentrale Funktionen verwendet: mapStateToProps für den Zugriff auf Store-Daten und mapDispatchToProps für das Dispatching von Actions.

30.1 Das Grundprinzip von connect()

Die connect() Funktion ist ein Higher-Order Component, das eine reguläre React-Komponente entgegennimmt und eine “verbundene” Version zurückgibt, die automatisch Zugriff auf den Redux Store hat. Dieser Ansatz folgt dem Container-Komponenten Pattern, bei dem die Geschäftslogik von der Präsentationslogik getrennt wird.

const ConnectedComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(YourComponent);

Die ursprüngliche Komponente YourComponent bleibt dabei völlig unabhängig von Redux und kann als reine Präsentationskomponente entwickelt und getestet werden. Sie erhält alle benötigten Daten und Funktionen über Props, ohne zu wissen, dass diese aus einem Redux Store stammen.

30.2 mapStateToProps - Zugriff auf Store-Daten

Die Funktion mapStateToProps definiert, welche Teile des Store-States als Props an die Komponente übergeben werden sollen. Sie wird bei jeder State-Änderung aufgerufen und ermöglicht es, nur die relevanten Daten zu extrahieren und bei Bedarf zu transformieren.

const mapStateToProps = (state: RootState) => ({
  user: state.auth.currentUser,
  isLoading: state.ui.isLoading,
  todoCount: state.todos.items.length,
  hasUnsavedChanges: state.todos.items.some(todo => todo.isDirty),
});

Diese Funktion ist ein reiner Selektor, der ausschließlich auf Basis des aktuellen States neue Props berechnet. Sie sollte keine Seiteneffekte haben und deterministisch sein. Komplexere Selektoren können hier implementiert werden, etwa die Berechnung abgeleiteter Daten oder die Filterung von Listen basierend auf anderen State-Teilen.

Ein häufiger Fehler besteht darin, in mapStateToProps den gesamten State oder große Objekte zu übergeben. Dies kann zu unnötigen Re-Renderings führen, da React eine oberflächliche Gleichheitsprüfung durchführt. Stattdessen sollten nur die wirklich benötigten Primitive oder kleine Objekte extrahiert werden.

Die Funktion kann auch einen zweiten Parameter ownProps erhalten, der die Props enthält, die von der Eltern-Komponente übergeben wurden:

const mapStateToProps = (state: RootState, ownProps: { userId: string }) => ({
  user: state.users.byId[ownProps.userId],
  isCurrentUser: state.auth.currentUserId === ownProps.userId,
});

30.3 mapDispatchToProps - Action-Dispatching

Die Funktion mapDispatchToProps definiert, welche Action-Creators als Props zur Verfügung gestellt werden. Es gibt zwei Syntaxformen: die Funktions-Form und die Objekt-Form.

Die Funktions-Form gibt vollständige Kontrolle über das Dispatching:

const mapDispatchToProps = (dispatch: Dispatch) => ({
  onLogin: (credentials: LoginCredentials) => dispatch(login(credentials)),
  onLogout: () => dispatch(logout()),
  onBulkUpdate: (updates: Update[]) => {
    updates.forEach(update => dispatch(updateItem(update)));
    dispatch(saveChanges());
  },
});

Die Objekt-Form ist kompakter und wird automatisch mit dispatch() umhüllt:

const mapDispatchToProps = {
  onLogin: login,
  onLogout: logout,
  updateItem,
};

Beide Formen sind äquivalent, wenn nur einfache Action-Creators dispatcht werden sollen. Die Funktions-Form bietet jedoch mehr Flexibilität für komplexe Dispatch-Logik oder die Kombination mehrerer Actions.

30.4 TypeScript-Integration und Typsicherheit

Die korrekte Typisierung von connect() war historisch eine Herausforderung, wurde aber mit dem ConnectedProps Utility deutlich vereinfacht:

const connector = connect(mapStateToProps, mapDispatchToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;

interface OwnProps {
  title: string;
}

type Props = PropsFromRedux & OwnProps;

const MyComponent: React.FC<Props> = ({ user, onLogin, title }) => {
  // Komponenten-Logik
};

export default connector(MyComponent);

Dieser Ansatz gewährleistet vollständige Typsicherheit, ohne die Props manuell definieren zu müssen. TypeScript kann automatisch ableiten, welche Props aus dem Store kommen und welche von der Eltern-Komponente übergeben werden.

30.5 Performance-Überlegungen

Das connect() Pattern implementiert automatische Optimierungen durch oberflächliche Gleichheitsprüfungen. Eine Komponente wird nur dann re-rendered, wenn sich die Referenzen der von mapStateToProps zurückgegebenen Props ändern. Dies bedeutet, dass die Rückgabe-Objekte konsistent strukturiert sein sollten.

Problematisch wird es, wenn in mapStateToProps neue Objekte oder Arrays erstellt werden:

// Problematisch - erstellt bei jedem Aufruf neue Arrays
const mapStateToProps = (state: RootState) => ({
  completedTodos: state.todos.filter(todo => todo.completed),
  todoStats: { total: state.todos.length, completed: state.todos.filter(todo => todo.completed).length },
});

Für solche Fälle sollten Memoization-Bibliotheken wie Reselect verwendet werden, die teure Berechnungen cachen und nur bei relevanten State-Änderungen neu berechnen.

30.6 Häufige Antipatterns und Fehlerquellen

Ein verbreiteter Fehler ist die Übergabe von Callback-Funktionen durch mapStateToProps, anstatt mapDispatchToProps zu verwenden. Dies führt zu neuen Funktions-Referenzen bei jedem Re-Render und bricht die Performance-Optimierungen.

Ebenso problematisch ist die direkte Manipulation von State-Objekten innerhalb der Mapping-Funktionen. Redux State sollte als unveränderlich behandelt werden, auch in Selektoren.

Ein weiterer häufiger Fehler besteht darin, zu viele Daten aus dem Store zu extrahieren. Jede Änderung an einem beliebigen Teil des extrahierten States führt zu einem Re-Render, auch wenn die Komponente diese spezifischen Daten gar nicht nutzt.

30.7 Der Übergang zu React Hooks

Mit der Einführung von React Hooks wurden useSelector und useDispatch als moderne Alternative zu connect() eingeführt. Diese bieten eine direktere und oft intuitivere API:

// Statt connect()
const MyComponent = () => {
  const user = useSelector((state: RootState) => state.auth.currentUser);
  const dispatch = useDispatch();
  
  const handleLogin = (credentials: LoginCredentials) => {
    dispatch(login(credentials));
  };
  
  return (/* JSX */);
};

Hooks bieten mehrere Vorteile: weniger Boilerplate-Code, bessere TypeScript-Integration, einfacheres Testing und die Möglichkeit, mehrere Selektoren in einer Komponente zu verwenden, ohne komplexe Mapping-Funktionen zu erstellen.

30.8 Migration von connect() zu Hooks

Die Migration bestehender connect()-Komponenten zu Hooks kann schrittweise erfolgen. Dabei sollte zunächst die Funktionalität 1:1 übertragen werden, bevor Optimierungen vorgenommen werden.

Bei der Migration ist besonders auf die Performance-Charakteristika zu achten. Während connect() automatisch optimiert, müssen bei Hooks gegebenenfalls React.memo() oder eigene Optimierungen implementiert werden.

30.9 Wann connect() noch relevant ist

Obwohl Hooks die moderne Empfehlung darstellen, bleibt connect() in bestimmten Szenarien relevant. Legacy-Codebases verwenden oft durchgängig das connect()-Pattern, und eine vollständige Migration ist möglicherweise nicht wirtschaftlich.

Zudem kann connect() in sehr komplexen Komponenten mit vielen Redux-Abhängigkeiten übersichtlicher sein, da die gesamte Store-Integration in den Mapping-Funktionen gekapselt ist, anstatt über die Komponente verteilt zu werden.

Das Verständnis von connect() ist auch für das Verständnis anderer HOC-basierter Bibliotheken und Patterns in der React-Welt wertvoll. Viele Konzepte wie die Trennung von Container- und Präsentationslogik bleiben auch in der Hook-Ära relevant.

30.10 Debugging und Entwicklungstools

Die React DevTools bieten spezielle Unterstützung für connect()-Komponenten und zeigen die HOC-Hierarchie klar an. Dies kann beim Debugging hilfreich sein, um zu verstehen, welche Props von Redux kommen und welche von Eltern-Komponenten.

Die Redux DevTools zeigen ebenfalls, welche Komponenten bei bestimmten Actions re-rendern, was bei der Performance-Analyse von connect()-basierten Anwendungen hilfreich ist.