Startseite / Artikel / Wie React-Redux entscheidet, ob er neu rendern soll: Selektoren, Egalität und typisierte Hooks

Wie React-Redux entscheidet, ob er neu rendern soll: Selektoren, Egalität und typisierte Hooks

Ein Lernleitfaden zu den Interna von React-Redux: Provider, useSelector-Egalität, gememorierte Selektoren, connect(), typisierte Hooks mit withTypes() sowie seltene Randfälle.

7738 Wörter

Die Oberfläche von React-Redux erscheint winzig: ein Provider-Komponente und ein paar Hooks. Doch in großen Codebasen tauchen immer wieder dieselben Fragen auf. Warum wird diese Komponente bei jedem Dispatch gerendert? Warum verhält sich ein Selector, der ein Objekt zurückgibt, anders als mapStateToProps? Wann ist shallowEqual das richtige Werkzeug, was beschreiben eigentlich RootState und AppDispatch, und ist batch() in React 18 noch notwendig?

Dieser Leitfaden beantwortet diese Fragen, indem er einen einzigen Ansatz verfolgt: Was geschieht zwischen einem versendeten Action und einer Neuerstellung der Darstellung? Sobald dieser Ablauf klar ist, sind die einzelnen APIs nicht mehr eine Liste zum Auswendiglernen, sondern Teil eines Systems, das man verstehen, debuggen und in Code-Reviews oder Interviews erklären kann.

useSelector()
useDispatch()
<Provider />

Der aktuelle Leitfaden: Die Betreuer von React-Redux empfehlen die Hooks-API als Standard für Komponenten. connect() wird weiterhin unterstützt und sollte trotzdem bekannt sein, da viele bestehende Codebasen darauf angewiesen sind.

Was React-Redux ist und wo es einzuordnen ist

Redux verwaltet den Zustand, führt Reducer aus und benachrichtigt Zuhörer; React rendernt die Benutzeroberfläche. React-Redux ist die offizielle Verbindung, die es Komponenten ermöglicht, aus dem Store zu lesen und Updates auszulösen, während React weiterhin für das Rendering verantwortlich ist.

Redux
  │
  │ Application State
  ▼
React-Redux
  │
  │ Integration
  ▼
React
  │
  │ UI
  ▼
User

Konkret verleiht diese Verbindung den Komponenten vier Fähigkeiten:

  • Das Auslesen von Werten aus dem Redux-Zustand
  • Das Abonnieren an Änderungen dieser Werte
  • Das Versenden von Aktionen an den Store
  • Das Einbinden von Store-Updates in Reacts Rendering-Zyklus

Die modernen Eingangspunkte für diese Aufgaben sind drei Hooks:

useSelector()
useDispatch()
useStore()

Zusätzlich gibt es das Komponenten, das den Store erst zugänglich macht:

<Provider />

Für älteren Code existiert außerdem die API für höherordentliche Komponenten, die weiterhin vollständig unterstützt wird:

connect()

Redux und React-Redux sind unterschiedliche Pakete

Redux selbst ist der Zustandscontainer und umfasst folgende Konzepte:

Store
State
Actions
Reducers
Dispatch
Middleware
Selectors

React-Redux dient lediglich als Brücke, und alles, was es exportiert, bezieht sich auf die Kommunikation mit React:

Provider
useSelector
useDispatch
useStore
connect

Zusammengesetzt sehen die Schichten so aus:

              Redux
        ┌───────────────┐
        │ Store         │
        │ State         │
        │ Reducers      │
        │ Actions       │
        │ Middleware    │
        └───────┬───────┘
                │
                ▼
          React-Redux
        ┌───────────────┐
        │ Provider      │
        │ useSelector   │
        │ useDispatch   │
        │ useStore      │
        │ connect       │
        └───────┬───────┘
                │
                ▼
              React

Falls eine Frage dazu geht, wie sich der Zustand ändert (Reducer, Middleware, Action-Strukturen), gehört sie zu Redux. Wenn es darum geht, wann eine Komponente einen Zustandswechsel wahrnimmt oder ausgibt, gehört sie zu React-Redux.

Der Kernzyklus, den man im Kopf behalten sollte

Bevor Sie eine einzige API verwenden, internalisieren Sie den Zyklus: Lesen Sie einen Selektor durch, leiten Sie bei Interaktion eine Aktion aus, reduzieren Sie den Zustand auf einen neuen Wert, führen Sie den Selektor erneut aus und rendern Sie nur, wenn sich der ausgewählte Wert geändert hat.

                 React Component
                       │
                       │ useSelector()
                       ▼
                 Redux Store
                       │
                       │ current state
                       ▼
                    Component
                       │
                       │ user interaction
                       ▼
                 useDispatch()
                       │
                       │ dispatch(action)
                       ▼
                    Reducer
                       │
                       ▼
                  New Redux State
                       │
                       ▼
                 useSelector()
                       │
                       ▼
                Re-render if
                 selected value
                   changed

Fast jede Leistungsfrage bezüglich React-Redux betrifft den letzten Schritt in diesem Zyklus.

Provider: Der Store zugänglich machen

Hooks können nur mit einem Store kommunizieren, der irgendwo oberhalb von ihnen im Baumstruktur vorhanden ist. <Provider> sorgt dafür, dass der Store an dieser Stelle platziert wird. In der Regel umhüllen Sie die Wurzel der Anwendung einmal:

import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  )
}

Im Hintergrund platziert der Provider den Store in einen React-Context. Jeder Nachkomme, egal wie tief er auch liegen mag, kann ihn anschließend ohne Prop-Drilling erreichen:

<Provider store={store}>
       │
       ├── App
       │    ├── Header
       │    ├── Dashboard
       │    ├── UserProfile
       │    └── Settings
       │
       └── Every descendant
            can use Redux

Falls eine Komponente einen der Hooks außerhalb eines Providers aufruft, gibt es keinen Store, aus dem gelesen werden kann, und der Hook fehlschlägt (in der Praxis wird ein Fehler ausgelöst, der mitteilt, dass der Kontextwert fehlt):

useSelector(...)
useDispatch(...)

Ein häufiger Ort, an dem das Problem auftritt, sind Unit-Tests, die eine Komponente ohne Einbettung in einen Test-Provider rendern.

useSelector: Wie Komponenten den Zustand lesen

useSelector() ist der Hook, den Sie am häufigsten aufrufen werden. Sie geben ihm eine Funktion, die den gesamten Redux-Zustand erhält und den Teil zurückgibt, den die Komponente benötigt:

const count = useSelector(
  state => state.counter.value
)

Die Struktur des Datenflusses ist einfach:

Redux State
     │
     ▼
useSelector()
     │
     ▼
Selected Value
     │
     ▼
React Component

Weil React-Redux Ihren Selector möglicherweise häufiger aufrufen kann, als Sie erwarten (beim Render, nach Dispatches, während Entwicklungsüberprüfungen), muss der Selector rein sein: gleiche Eingabe, gleiche Ausgabe, keine Nebeneffekte.

Was der Hook bei jedem Render und Dispatch tut

Nehmen Sie einen Selektor, der den aktuellen Benutzer auswählt:

const user = useSelector(
  state => state.auth.user
)

Hinter dieser einen Zeile führt React-Redux eine kleine Routine aus:

  1. liest den aktuellen Store-Zustand
  2. ruft Ihren Selektor damit auf
  3. speichert den zurückgegebenen Wert als „letztes ausgewähltes“ Ergebnis
  4. registriert eine Abonnierung am Store für dieses Komponente
  5. führt den Selektor nach jeder gesendeten Aktion erneut aus
  6. vergleicht das vorherige Ergebnis mit dem neuen
  7. plant eine Neuzeichnung nur dann, wenn der Vergleich Unterschiede zeigt

Schritt 6 ist die Quelle für fast alle überraschenden Verhaltensweisen.

Der Standardvergleich ist die strenge Referenzgleichheit

Aus der Box heraus vergleicht der Hook

useSelector()

die Ergebnisse mit ===:

previousResult === newResult

Wenn der Vergleich ein Ergebnis liefert

true

Der Selektor gibt React-Redux keinen Grund, das Komponenten wieder anzuzeigen. Wenn er etwas zurückgibt

false

wird die Neuanzeige der Komponente geplant.

Das ist ein bewusster Unterschied zu connect(), das das von mapStateToProps zurückgegebene Objekt oberflächlich vergleicht. Code, der unter connect() funktionierte, kann bei Portierung zu Hooks ohne Anpassung des Selektors bei jeder Aktion neu angezeigt werden.

Warum die Rückgabe eines neuen Objekts jedes Mal zu einer Neuanzeige führt

Ein natürlicher erster Versuch, zwei Werte zu lesen, sieht so aus:

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

Mit den Werten selbst ist alles in Ordnung, aber die Pfeilfunktion erstellt jedes Mal ein völlig neues Objektliteral:

{
  user: ...,
  count: ...
}

Zwei Objektliterale mit identischem Inhalt sind dennoch zwei verschiedene Objekte, sodass die Überprüfung

oldObject === newObject

immer zu

false

Die Folge ist eine Kette, die bei jeder Aktion in der App ausgelöst wird, einschließlich Aktionen, die nichts mit Benutzern oder Zählungen zu tun haben:

Action dispatched
      ↓
Selector executes
      ↓
New object created
      ↓
Different reference
      ↓
Component re-renders

Das gehört zu den häufigsten Leistungsproblemen bei React-Redux. In der Dokumentation werden drei Lösungen aufgeführt: getrennte Selektoren, eine benutzerdefinierte Gleichheitsfunktion wie shallowEqual oder ein gememorisierter Selektor.

Lösung eins: useSelector einmal pro Wert aufrufen

Anstatt die Werte in ein Objekt zusammenzufassen,

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

teilen Sie die Lesevorgänge in unabhängige Hooks auf:

const user = useSelector(
  state => state.user
)
const count = useSelector(
  state => state.count
)

Jeder Selektor gibt nun einen Wert zurück, der bereits im Store vorhanden ist, und nicht eine neue Umhüllung darum. Wenn sich der Zustand, der sie enthält, nicht geändert hat

user unchanged
count unchanged

sind die Referenzen identisch und die ===-Prüfung besteht.

Es gibt keine Strafe dafür, den Hook mehrmals in einem Component aufzurufen. Wenn eine einzige Dispatch-Aktion mehr als einen der ausgewählten Werte ändert, gruppiert React-Redux die resultierenden Aktualisierungen, sodass der Component für diese Dispatch-Aktion nur einmal gerendert wird und nicht einmal pro Hook.

Lösung zwei: shallowEqual für gezielte Objekte

Mannchmal ist ein Objekt tatsächlich die klarste Möglichkeit, etwas zurückzugeben – beispielsweise dann, wenn ein Component eine kleine Gruppe zusammenhängender Felder verarbeitet. In diesem Fall sollte shallowEqual als Vergleichsmechanismus übergeben werden:

import {
  useSelector,
  shallowEqual
} from 'react-redux';

const data = useSelector(
  state => ({
    user: state.user,
    count: state.count
  }),
  shallowEqual
)

Dadurch vergleicht React-Redux jedes oberflächliche Feld der alten und neuen Objekte mit === anstelle der Vergleich der beiden Objektreferenzen. Ein neues Wrapper mit denselben Feldwerten gilt als unverändert.

Neue Versionen unterstützen den Vergleich auch über ein Options-Objekt:

const data = useSelector(
  selector,
  {
    equalityFn: shallowEqual
  }
)

Wann shallowEqual sinnvoll ist

Wenden Sie shallowEqual an, wenn das Gruppieren von Werten die Komponente übersichtlicher macht. Behandeln Sie es nicht als Standard, der bei jedem Aufruf automatisch angewendet wird:

useSelector(selector, shallowEqual)

Die bessere erste Frage ist, ob die Komponente nicht einfach jeden Wert einzeln auswählen kann. Meistens ist das möglich, und das Ergebnis lässt sich leichter durchsehen:

const user = useSelector(
  state => state.auth.user
)

const permissions = useSelector(
  state => state.auth.permissions
)

Denken Sie daran, dass shallowEqual nur eine Ebene tief prüft. Wenn ein Feld im zurückgegebenen Objekt selbst ein neu erstelltes Array oder Objekt ist, scheitert der Vergleich dennoch.

Selektoren, die Daten ableiten

Selektoren dienen nicht nur dazu, Felder auszuwählen. Sie sind auch der Ort, an dem abgeleitete Daten berechnet werden, wie beispielsweise eine gefilterte Liste:

const selectCompletedTodos = state =>
  state.todos.filter(
    todo => todo.completed
  )

Das Problem ist jedoch

filter()

dass immer ein neues Array allokiert wird. Selbst dann, wenn die To-Do-Liste in keiner wesentlichen Weise geändert wurde,

oldArray !== newArray

Es wird der Wert gespeichert, und das Komponente wird nach jeder Aktion neu gerendert. Die Memoisierung löst dieses Problem, indem sie die Ausgabe anhand der Eingaben speichert. Wenn die Eingaben dieselben Objekte wie beim letzten Mal sind, wird das gespeicherte Ergebnis zurückgegeben:

Same inputs
    ↓
Return cached result

Wenn sich eine Eingabe geändert hat, wird die Berechnung erneut ausgeführt:

Changed inputs
    ↓
Recalculate

createSelector von Reselect

Das Standardwerkzeug dafür ist createSelector von Reselect (Redux Toolkit gibt es erneut frei). Man listet die Eingabenselektoren auf und gibt anschließend eine Ergebnisfunktion an, die nur dann ausgeführt wird, wenn sich diese Eingaben ändern:

import { createSelector } from 'reselect';

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos =>
    todos.filter(todo => todo.completed)
)

Das Komponente verwendet es wie jeden anderen Selektor:

const todos = useSelector(
  selectCompletedTodos
)

Da state.todos dieselbe Array-Referenz ist, gibt der Selektor weiterhin das zuvor gefilterte Array zurück, wodurch === zutrifft. Die Einschränkung: Ein memoisierter Selektor besitzt einen Cache, weshalb der Ort seiner Erstellung wichtig ist.

Erstellen Sie selektierte, nur auf den Zustand bezogene Memoisierer einmal auf Modulebene

Wenn ein memoisierter Selektor nur vom Redux-Zustand abhängt,

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos => ...
)

deklarieren Sie ihn außerhalb jeder Komponente:

const selectCompletedTodos = createSelector(...)

und verweisen Sie von der Komponente darauf:

function TodoList() {
  const todos = useSelector(
    selectCompletedTodos
  )
}

Falls createSelector innerhalb des Komponentenkörpers ausgeführt wird, erzeugt jede Renderung einen neuen Selektor mit einem leeren Cache – die Memoisierung nützt dann überhaupt nichts. Eine Instanz auf Modulebene bleibt über mehrere Renderungen hinweg bestehen.

Memoisierter Selektoren, die auch Props benötigen

Einfache, nicht-memoisierte Selektoren, die Props lesen, sind unbedenklich. Solche Selektoren speichern keinen Cache, daher kann nichts schiefgehen:

function TodoListItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

Es wird komplizierter, sobald ein memoisierter Selektor auf beide Faktoren angewiesen ist.

Redux State + Component Props

Ein solcher Selektor speichert das Ergebnis für seine letzten Argumente. Wenn viele Listeneinträge eine Instanz teilen, wobei jeder einen anderen id hat, invalidieren sie ständig gegenseitig ihren Cache. Für ein einzelnes Komponenten ist es in der Regel ausreichend, den Selektor mit useMemo zu erstellen; bei vielen Komponenten muss man die Memoisierungsstrategie der Bibliothek kennen (Cache-Größe oder eine Instanz pro Komponente). Dies kommt in fortgeschrittenen Interviews sowie bei langen Listen zur Sprache.

Selektoren müssen rein bleiben

Ein Selektor sollte eine einfache Funktion des Zustands sein:

State
  ↓
Selector
  ↓
Value

und niemals ein Ort, an dem Arbeit in die Umwelt „durchsickert“:

State
  ↓
Selector
  ↓
API call
  ↓
Mutation
  ↓
Side effect

Der folgende Typ von Selektor sollte vermieden werden; Logging, Netzwerkaufrufe und Mutationen gehören hier nicht hin:

const selectUser = state => {
  console.log('side effect')
  // API call ❌
  // mutation ❌
  return state.user
}

Verwendung von Props innerhalb eines Selektors

Ein in einer Komponente inline definierte Selektor kann einfach auf die Props der Komponente zugreifen:

function TodoItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

Der Wert gelangt von der Eigenschaft über die Schließfunktion zum Selektor:

id
 ↓
closure
 ↓
selector
 ↓
state.todos[id]

Das ist ein echter Unterschied zu mapStateToProps, das ownProps als zweites Argument erhält. useSelector() übermittelt überhaupt keine Eigenschaften, weshalb man auf Schließfunktionen oder Selektor-Factory-Funktionen angewiesen ist, die zusätzliche Argumente entgegennehmen.

useDispatch: Senden von Aktionen

Falls useSelector() die Leseseite darstellt,

useSelector
     ↓
    READ

dann ist useDispatch() die Schreibseite:

useDispatch
     ↓
  DISPATCH

Es gibt die dispatch-Funktion des Stores zurück, die man aus Ereignishandlern aufruft:

const dispatch = useDispatch()

function handleClick() {
  dispatch(increment())
}

oder direkt in JSX:

<button
  onClick={() => dispatch(increment())}
>
  Increment
</button>

Was durch Dispatch in Gang gesetzt wird

Die vollständige Verfolgung eines Klicks unterstreicht den zuvor erläuterten Kernzyklus:

User clicks button
       ↓
dispatch(action)
       ↓
Redux
       ↓
Reducer
       ↓
New state
       ↓
useSelector()
       ↓
Component updates

Ein konkreter Aufruf mit einem Aktionsersteller und einem Payload sieht so aus:

dispatch(
  addTodo({
    id: 1,
    text: 'Learn Redux'
  })
)

Stabile Callbacks für gememorierte Kinderkomponenten

Die meisten Dispatch-Callbacks benötigen useCallback nicht. Die Ausnahme tritt ein, wenn ein Handler wie

const increment = () =>
  dispatch(incrementAction())

an eine in React.memo eingehüllte Kinderkomponente weitergegeben wird:

<MyButton
  onIncrement={increment}
/>

Weil die Pfeilfunktion bei jeder Renderung des Elternteils neu erstellt wird, erhält die gememorierte Kinderkomponente jedes Mal neue Eigenschaften und rendernt dennoch. Das Einhüllen des Handlers behebt dieses Problem:

const increment = useCallback(
  () => dispatch(incrementAction()),
  [dispatch]
)

zusammen mit einer gememorierten Kinderkomponente:

const MyButton = React.memo(...)

Es ist sicher, dispatch als Abhängigkeit aufzulisten: Seine Identität bleibt dieselbe, solange der Provider dieselbe Store-Instanz erhält.

useStore: direkter Zugriff, selten benötigt

Der dritte Hook gibt Ihnen das Store-Objekt selbst zur Verfügung:

const store = useStore()

Dadurch können Sie die rohen Store-Methoden aufrufen:

store.getState()
store.dispatch(...)
store.subscribe(...)

Derartiger Zugriff wird fast nie von einer Komponente zur Darstellung von Daten verwendet. Das Lesen erfolgt über

useSelector()

und das Schreiben erfolgt über

useDispatch()

In den Dokumentationen wird useStore() als Notlösung für seltene Fälle wie das Einbinden eines Reducers betrachtet, nicht für alltägliche Lesevorgänge.

Warum store.getState() in render veraltet wird

Betrachten Sie eine Komponente, die direkt aus dem Store liest:

function Component() {
  const store = useStore()
  const user = store.getState().user
  return <div>{user.name}</div>
}

Diese wird einmal korrekt dargestellt und bleibt danach zurück. getState() ist ein einmaliger Lesevorgang ohne Abonnement, sodass spätere Änderungen React nicht dazu bringen, diese Komponente erneut darzustellen. Die mit einem Abonnement versehene Version bleibt im Einklang:

const user = useSelector(
  state => state.user
)

Die drei Hooks im Überblick

┌────────────────────────────┐
│ React-Redux Hooks          │
├────────────────────────────┤
│ useSelector()              │ → READ
│ useDispatch()              │ → DISPATCH
│ useStore()                 │ → STORE ACCESS
└────────────────────────────┘

In der alltäglichen Komponenten-Entwicklung übernehmen die ersten beiden fast die gesamte Arbeit:

90%+
useSelector()
useDispatch()

useStore() kommt nur gelegentlich zum Einsatz.

connect(): die API für höherer Ordnung

Hooks sind der empfohlene Ansatz, doch connect() ist weiterhin vorhanden, und viele langfristig genutzte Codebasen basieren darauf. Man wird auf solchen Code stoßen:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Wie connect den Store in Props umwandelt

Das mentale Modell ist ein Wrapper, der Store-Daten und Dispatch-Funktionen in gewöhnliche Props umwandelt:

Redux Store
     │
     ▼
connect()
     │
     ├── mapStateToProps
     │
     └── mapDispatchToProps
     │
     ▼
Component Props

mapStateToProps erhält den Zustand und gibt ein Objekt mit Props zurück:

const mapStateToProps = state => ({
  user: state.auth.user,
  count: state.counter.value
})

Im Grunde führt es diese Umwandlung durch:

Redux State
     ↓
Component Props

Die umgewickelte Komponente bleibt eine einfache Funktion ihrer Props und weiß nicht, dass Redux existiert:

function User({ user, count }) {
  return (
    <div>
      {user.name}
      {count}
    </div>
  )
}

mapDispatchToProps stellt die Callbacks bereit. In seiner Funktionsform erhält man dispatch und muss die Handler selbst erstellen:

const mapDispatchToProps =
  dispatch => ({
    increment: () =>
      dispatch(increment())
  })

Dann ruft die Komponente diese als Props auf:

props.increment()

Die Objektschreibweise für mapDispatchToProps

Die kürzere Form übermittelt ein Objekt mit Action-Creatern:

const mapDispatchToProps = {
  increment,
  decrement
}

React-Redux bindet jeden Action-Creator, sodass ein Aufruf des Props diesen auslöst. Dies ist in der Regel die übersichtlichere Option.

Die vier gängigen Formen von connect

Es gibt vier Varianten. Ohne Argumente erhält die Komponente nur dispatch als Prop:

connect()(Component)

Mit nur einem State-Mapper liest die Komponente Daten, erhält aber keine gebundenen Action-Creators:

connect(
  mapStateToProps
)(Component)

Wenn null als erstes Argument verwendet wird, hört das Komponente niemals auf den Store und erhält nur Dispatch-Eigenschaften:

connect(
  null,
  mapDispatchToProps
)(Component)

Und bei Verwendung beider Parameter liest es Daten ein und sendet sie weiter:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Es ist nützlich zu wissen, dass die Verwendung von null die Abonnierung überspringt – es handelt sich dabei um eine einfache Methode, einem Komponenten Zugriff auf Dispatch zu geben, ohne dass es bei Store-Änderungen gerendert wird.

connect gibt eine neue Komponente zurück

Durch Aufruf von

connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

wird MyComponent nicht verändert. Es wird eine separate Wrapper-Komponente erzeugt, die Ihre Komponente darin rendernt:

MyComponent
     │
     ▼
connect()
     │
     ▼
ConnectedComponent

Deshalb exportieren verbundene Module in der Regel die gewickelte Version standardmäßig und exportieren manchmal die einfache Komponente separat für Tests.

Auswahl zwischen Hooks und connect

Für eine neue Anwendung ist die Antwort einfach:

New React application
        ↓
Hooks

Für ein bereits vorhandenes Beispiel ist die Antwort pragmatisch:

Existing connect()
        ↓
Understand and maintain it

Hooks bedeuten weniger Boilerplate, keine Wrapper-Komponenten und einen viel einfacheren TypeScript-Unterbau – deshalb sind sie Standard. Komponenten, die bereits mit connect() arbeiten, müssen nicht neu geschrieben werden; konvertieren Sie sie einfach, wenn Sie ohnehin an ihnen arbeiten.

Der Unterschied bei Gleichheitsprüfungen in einer Zeile

Behalten Sie diese Kombination im Hinterkopf:

useSelector()
      ↓
=== reference equality

versus

connect()
      ↓
shallow equality

Viele „Es hat vor der Refaktorierung funktioniert“-Bugs entstehen dadurch: Ein neues Objekt war in mapStateToProps sicher, scheitert aber an === in useSelector().

Typisierung von React-Redux mit TypeScript

React-Redux liefert eigene Typdefinitionen, und die Dokumentation beschreibt eine standardmäßige typisierte Einrichtung. Sie basiert auf sechs Begriffen:

RootState
AppDispatch
AppStore
useAppSelector
useAppDispatch
useAppStore

RootState: Den Zustandstyp aus dem Store ableiten

Angenommen, ein Store ist mit Redux Toolkit konfiguriert,

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer
  }
})

erhält man den Zustandstyp aus dem, was getState zurückgibt:

export type RootState =
  ReturnType<typeof store.getState>

Das ist besser, als die Struktur manuell zu definieren,

type RootState = {
  counter: CounterState
  users: UsersState
}

denn der abgeleitete Typ folgt automatisch den Reduzern.

AppDispatch: Der Dispatch-Typ inklusive Middleware

Der Dispatch-Typ wird auf dieselbe Weise abgeleitet:

export type AppDispatch =
  typeof store.dispatch

Der einfache Redux Dispatch-Typ kennt nur einfache Aktionen; der abgeleitete Typ berücksichtigt Ihre Middleware, was wichtig ist, sobald Sie verwenden:

  • Middleware, die verändern, was dispatch akzeptiert
  • Thunks
  • einen angepassten Dispatch
  • asynchrone Aktionen jeglicher Art

Ohne AppDispatch führt das Dispatchen eines Thunks zu einem Typfehler.

AppStore: Der Typ des Stores selbst

Der Store-Typ wird durch eine weitere Inferenz ermittelt:

export type AppStore =
  typeof store

Dadurch hat jede Aufgabe eine einzige Quelle der Wahrheit. Der Zustandstyp:

RootState
    ↓
state type

Der Dispatch-Typ:

AppDispatch
    ↓
dispatch type

Der Store-Typ:

AppStore
    ↓
store type

AppStore ist besonders nützlich, wenn Sie für jede Anfrage oder jeden Test einen Store erstellen und diesen weitergeben müssen.

Vorvorgefertigte Hooks mit withTypes()

Ab React-Redux 9.1.0 bietet jeder Hook eine .withTypes()-Methode:

useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()

Das dokumentierte Muster erstellt einmal Hooks, die speziell für die Anwendung geeignet sind:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Was typisierte Hooks Ihnen ersparen

Ohne sie benötigt jeder Selector eine explizite Anmerkung:

const user = useSelector(
  (state: RootState) =>
    state.auth.user
)

Mit dem typisierten Hook,

const user = useAppSelector(
  state => state.auth.user
)

weiß der Compiler bereits

state = RootState

Auch die Dispatch-Funktion arbeitet auf dieselbe Weise:

const dispatch = useAppDispatch()

Diese dispatch-Funktion akzeptiert Thunks sowie alles andere, was Ihr Middleware-System zulässt, wobei eine vollständige Überprüfung erfolgt.

Ein hooks.ts-Datei für die Anwendung

In typischen Projekten werden diese Elemente in einem kleinen Modul zusammengefasst:

import {
  useDispatch,
  useSelector,
  useStore
} from 'react-redux'

import type {
  RootState,
  AppDispatch,
  AppStore
} from './store'

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Komponenten importieren von diesem Modul statt direkt von react-redux:

const user = useAppSelector(
  state => state.auth.user
)

const dispatch = useAppDispatch()

ConnectedProps für typisierten connect()-Code

Codebasen, die connect()-Komponenten typisieren, enthalten

ConnectedProps

Trennen Sie den connect-Aufruf zunächst in einen Connector auf und extrahieren Sie anschließend die von ihm injizierten Props:

const connector = connect(
  mapState,
  mapDispatch
)

type PropsFromRedux =
  ConnectedProps<typeof connector>

PropsFromRedux beschreibt genau, was der Connector injiziert, sodass die mappierten Typen niemals doppelt erstellt werden.

Gute Selektoren entwerfen

Es ist verlockend, einen Selector lediglich als etwas zu betrachten, das

state => state.user

In einer größeren Anwendung sollten Selektoren eher als Grenze betrachtet werden, die das Speicherformat des Zustands in die von der Benutzeroberfläche gewünschte Form umwandelt:

Redux State
     ↓
Selector
     ↓
UI-friendly data

Zum Beispiel kann die Regel, welche Todo-Einträge sichtbar sein sollen, in einer benannten Funktion untergebracht werden:

const selectVisibleTodos =
  state =>
    state.todos.filter(
      todo => !todo.hidden
    )

und die Komponente fragt einfach nach dem Ergebnis:

const todos = useSelector(
  selectVisibleTodos
)

Die Komponente bleibt rein präsentational, und die Regel kann unabhängig getestet werden.

Ein guter Selektor ist präzise

const selectUserName =
  state => state.auth.user.name

Er gibt genau das zurück, was die Komponente anzeigt, und nichts weiter, wodurch nur bei einem Namenwechsel eine Neuansicht ausgelöst wird.

Den gesamten Zustand auszuwählen, ist fast immer falsch

const selectEverything =
  state => state

Redux erzeugt ein neues Wurzelzustandsobjekt, sobald ein Reducer etwas ändert. Ein Selektor, der die Wurzel zurückgibt, liefert daher nach praktisch jeder Aktion einen neuen Referenzwert:

Anything in Redux changes
        ↓
Root state reference changes
        ↓
Selector result changes
        ↓
Component re-renders

Die Entwicklungsüberprüfungen von React-Redux melden dieses Muster.

Halten Sie die Selektoren präzise

Ziehen Sie mehrere gezielte Abfragen vor:

const count =
  useSelector(
    state => state.counter.value
  )

const user =
  useSelector(
    state => state.auth.currentUser
  )

anstelle einer allumfassenden Abfrage:

const state =
  useSelector(state => state)

Eine praktische Faustregel:

Wählen Sie den kleinsten Teil des Zustands aus, den das Komponente tatsächlich verwenden kann.

Selektoren-Überprüfungen im Entwicklungsmodus

Neue Versionen von React-Redux führen bei Entwicklungsbuilds zusätzliche Überprüfungen Ihrer Selektoren durch. Zwei davon sind besonders wichtig zu kennen.

Die Stabilitätsprüfung

Bei der ersten Überprüfung wird Ihr Selektor mit dem gleichen Zustand ein zweites Mal aufgerufen und die Ergebnisse verglichen:

selector(state)
      ↓
run again with same state
      ↓
same result?

Falls das Ergebnis

same reference

ist der Selektor stabil. Falls nicht

new reference

React-Redux gibt eine Warnung aus, weil ein Selector, der für identische Eingaben einen neuen Referenzwert zurückgibt, seine Komponente bei jeder Store-Änderung neu rendern wird.

Ein typisches Problem ist der zuvor erwähnte Objekt-Literal-Selector:

const data = useSelector(
  state => ({
    count: state.count,
    user: state.user
  })
)

Da das Objekt bei jedem Aufruf neu erstellt wird, erkennt die Überprüfung Folgendes:

same input
    ↓
different object
    ↓
unstable selector

Konfigurieren, wie oft die Überprüfungen ausgeführt werden

Die Häufigkeit für die gesamte Anwendung können Sie im Provider festlegen:

<Provider
  store={store}
  stabilityCheck="always"
>
  <App />
</Provider>

oder sie für einen einzelnen Hook-Aufruf überschreiben:

const count = useSelector(
  selectCount,
  {
    devModeChecks: {
      stabilityCheck: 'once'
    }
  }
)

Die zulässigen Werte sind:

never
once
always

Der Standardwert ist 'once', was bedeutet, dass die Überprüfung beim ersten Aufruf jedes Hooks erfolgt. Nichts davon wird in Produktionsbauen ausgeführt.

Die Überprüfung der Identitätsfunktion

Die zweite Überprüfung sucht nach einem Selector, der seine Eingabe unverändert zurückgibt:

state => state

In einem Komponenten sieht das so aus:

const state = useSelector(
  state => state
)

Dies verbindet die Komponente mit jeder Änderung im Store:

Any Redux change
       ↓
Root state changes
       ↓
Component re-renders

In den Dokumentationen wird dies als Identitätsfunktionsprüfung bezeichnet. In früheren Versionen hieß es noopCheck, was man noch in älteren Konfigurationen finden kann.

Die Lösung ist wie zuvor: ersetzen

const state = useSelector(
  state => state
)

durch das Auslesen der spezifischen Werte, die man benötigt:

const count = useSelector(
  state => state.counter.value
)

const user = useSelector(
  state => state.auth.currentUser
)

Rendering und Leistung jenseits der Selektoren

Das Rendering des Elternteils wirkt sich weiterhin aus

useSelector() steuert nur die durch Store-Updates verursachten Renderings. Es hat keinen Einfluss auf die normale React-Regel, dass eine Komponente rendernt, wenn ihr Elternteil rendernt:

Parent renders
      ↓
Child renders

Das tritt auch dann ein, wenn sich der Redux-Zustand überhaupt nicht geändert hat. Wenn ein Kind aufwändig zu verarbeiten ist und seine Props stabil sind, sollte es in etwas eingebettet werden.

React.memo()

Dies unterscheidet sich von connect(), dessen Wrapper sich wie ein gememorisierter Komponente verhält; komponentenbasierte Hooks bieten kein solches Verhalten kostenlos.

Kombination von React.memo mit useSelector

Hier abonniert die Komponente einen Zähler und wird außerdem hinsichtlich ihrer name-Eigenschaft gememorisiert:

const Counter = ({ name }) => {
  const count = useSelector(
    state => state.counter.value
  )

return (
    <div>
      {name}: {count}
    </div>
  )
}
export default React.memo(Counter)

Die beiden Mechanismen decken die beiden Ursachen für Render-Aufrufe ab:

Redux selector
       +
React.memo
       ↓
More controlled rendering

Die Memorisierung hat ihren eigenen Aufwand, daher analysieren Sie zunächst die Leistung. Für eine umfassendere Übersicht der Auslöser für Render-Aufrufe sehen Sie unsere Anleitung zu gängigen Mustern, die unnötige React-Neurenderungen verursachen.

Ein Leistungsmodell, das auf einem Bildschirm passt

Nach jeder Aktion läuft React-Redux die Selektoren der angemeldeten Komponenten erneut ab, vergleicht jedes Ergebnis mit dem vorherigen und rendernt nur die Komponenten, deren Ergebnisse sich unterscheiden:

Redux action
     ↓
Store updates
     ↓
Selectors execute
     ↓
Selector results compared
     ↓
Changed?
 ┌───┴────┐
No       Yes
│         │
│         ▼
│      Re-render
│
└── No Redux-triggered render

Ihre Aufgabe besteht also darin, Selektoren zu schreiben, die:

  1. nur die Daten zurückgeben, die die Komponente verwendet
  2. wenn sich nichts Relevantes geändert hat, stabile Referenzen zurückgeben
  3. ohne Not neue Objekte oder Arrays zu erstellen
  4. teure zu berechnende abgeleitete Daten zu memoisieren

Regel eins: Wählen Sie niemals den Root-State aus

useSelector(state => state)

Regel zwei: Vermeiden Sie das Erstellen von Wrapper-Objekten in Selektoren

Ein solcher Selector erstellt bei jedem Ausführungsvorgang neue Objekte:

useSelector(state => ({
  user: state.user,
  count: state.count
}))

Geben Sie ein solches Objekt nur dann zurück, wenn Sie es absichtlich mit

shallowEqual

oder wenn Sie es durch einen memoisierten Selector leiten.

Regel drei: Memoisieren Sie teure Ableitungen

Sortieren, Filtern, Gruppieren und Verknüpfen gehören dazu

createSelector(...)

Regel vier: Wenden Sie React.memo auf die relevanten Komponenten an

Vor der Memoisierung einer Komponente gehen Sie eine kurze Checkliste durch:

Is the component expensive?
       ↓
Does it receive stable props?
       ↓
Does it re-render unnecessarily?
       ↓
Then consider React.memo()

Falls Ihre Codebasis den React Compiler verwendet, wird ein Großteil dieser manuellen Memoisierung bereits automatisch erledigt.

Regel fünf: Wählen Sie so eng wie möglich – innerhalb der Möglichkeiten der Komponente

Zu breit:

state => state

Besser:

state => state.auth.user

Noch besser, wenn die Komponente nur den Namen anzeigt:

state => state.auth.user.name

Seltene Randfälle: veraltete Props und „Zombie“-Kinderkomponenten

Die meisten Anwendungen stoßen niemals auf diese beiden Probleme, doch sie erklären, warum defensive Selektoren eine gute Gewohnheit sind.

Veraltete Props

Bei Problemen mit veralteten Props benötigt man einen Selektor, der von einem Prop abhängt, sowie eine Store-Änderung, die sowohl den Zustand als auch indirekt diesen Prop ändert:

Selector depends on component props
        ↓
Redux action updates state
        ↓
Parent would receive new props
        ↓
Child selector runs first
        ↓
Selector sees old props

Die Abonnementverwaltung des Kind-Elements kann ausgelöst werden, bevor das Elternelement neu gerendert wurde und den neuen Wert weitergegeben hat. Für einen kurzen Moment kombiniert der Selektor den aktualisierten Zustand mit einem alten Wert. Ein typisches Szenario ist:

const todo = useSelector(
  state => state.todos[props.id]
)

Falls das Element mit dieser id gerade entfernt wurde oder das Elternelement kurz darauf eine andere id weitergeben will, liest der Selektor vorübergehend Daten, die nicht mehr zutreffen.

Selektoren schreiben, die fehlende Daten tolerieren

Die anfällige Version geht davon aus, dass das Element immer vorhanden ist:

state.todos[props.id].name

Eine defensive Version sucht zunächst nach dem Element,

const todo =
  state.todos[props.id]

und liest erst danach darin.

return todo
  ? todo.name
  : undefined

Die Entscheidung erfolgt durch eine einfache Prüfung:

Does data exist?
      ↓
Yes → use it
No  → handle safely

Optional Chaining (state.todos[id]?.name) drückt dieselbe Idee in einer einzigen Ausdrucksformulierung aus.

Zombie-Kinder-Elemente

Das Szenario mit dem Zombie-Kind beinhaltet einen Elternteil, der eine Liste darstellt, und ein Kind, das sich für ein bestimmtes Element anmeldet:

Parent
  │
  └── Child

Der Ablauf verläuft wie folgt:

  1. Das Kind meldet sich beim Store an.
  2. Eine Aktion löscht die Daten, die das Kind anzeigt.
  3. Der Elternteil wird bei der nächsten Darstellung dieses Kindes nicht mehr darstellen.
  4. Vorher läuft jedoch noch die Anmeldung des Kindes ab.
  5. Der Selektor des Kindes versucht, auf Daten zuzugreifen, die bereits verschwunden sind.

An diesem letzten Schritt tritt ein Fehler auf, wenn kein Schutz vorliegt. React-Redux verfügt über Mechanismen für Fehler bei Selektoren, die durch Store-Änderungen verursacht werden, doch defensive Selektoren bleiben die zuverlässigste Lösung.

Warum Hooks anfälliger sind als connect

connect() erstellt einen verschachtelten Abonnementsbaum: Jede verbundene Komponente wird nur aktualisiert, nachdem ihre vorgelagerten verbundenen Komponenten es bereits getan haben, wodurch eine top-down-Reihenfolge gewährleistet wird. Hooks hingegen werden direkt an den Store angehängt, ohne diese Hierarchie – daher sind die Garantien bezüglich der Reihenfolge schwächer und solche Randfälle werden theoretisch möglich.

Betrachten Sie dies als Hintergrundwissen und nicht als Grund, Hooks zu vermeiden. Laut der offiziellen Dokumentation treten diese Probleme in echten Anwendungen selten auf.

Erweiterte Funktionen des Providers

Ein benutzerdefinierter Kontext für isolierte Stores

Standardmäßig

<Provider store={store}>

veröffentlicht den Store über den eingebauten Kontext von React-Redux. Eine wiederverwendbare Komponentenbibliothek, die intern Redux verwendet, kann auf diese Weise mit dem Store der Host-Anwendung in Konflikt geraten. Um dies zu vermeiden, akzeptiert der Provider einen eigenen Kontext:

<Provider
  context={MyContext}
  store={myStore}
>

Die an diesen Kontext gebundenen Hooks stammen aus Factory-Funktionen:

createStoreHook()
createDispatchHook()
createSelectorHook()

Das ist hauptsächlich für wiederverwendbare Bibliotheken wichtig, bei denen sonst mehrere Speicherinstanzen kollidieren würden.

batch() und die automatische Batching-Funktion in React 18

Älterer Code umschließt oft aufeinanderfolgende Dispatch-Aufrufe in batch(), damit React nur einmal statt zweimal rendernt:

batch(() => {
  dispatch(action1())
  dispatch(action2())
})

React 18 batcht Updates automatisch, einschließlich solcher in Promises und Timeout-Funktionen, sodass eine typische React 18-Anwendung dafür kein batch() benötigt. Neuere React-Redux-Versionen behalten es größtenteils aus Kompatibilitätsgründen bei; prüfen Sie die aktuellen Dokumentationen und rechnen Sie damit, dass es in älterem Code vorhanden ist.

serverState für Server-Side Rendering und Hydratierung

Für das Server-Side Rendering nimmt der Provider ein zusätzliches Attribut entgegen:

<Provider
  store={store}
  serverState={preloadedState}
>

Der Server rendernt HTML aus einem Anfangszustand und sendet diesen Zustand an den Browser. serverState sorgt dafür, dass die Hydratierungsrenderung denselben Snapshot verwendet und somit Abweichungen verhindert:

Server
  ↓
Initial Redux State
  ↓
HTML
  ↓
Browser Hydration
  ↓
Provider(serverState)
  ↓
Consistent initial render

Ein typisches Projektlayout

Eine auf Redux Toolkit basierende TypeScript-Anwendung ist oft nach Funktionalitäten strukturiert, wobei die Verkabelung des Stores an einem Ort erfolgt:

src/
│
├── app/
│   ├── store.ts
│   └── hooks.ts
│
├── features/
│   │
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   └── Counter.tsx
│   │
│   ├── users/
│   │   ├── usersSlice.ts
│   │   └── Users.tsx
│   │
│   └── auth/
│       ├── authSlice.ts
│       └── Login.tsx
│
├── App.tsx
└── main.tsx

store.ts

Das Store-Modul konfiguriert die Reduzierer und exportiert die abgeleiteten Typen:

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
    auth: authReducer
  }
})

export type RootState =
  ReturnType<typeof store.getState>

export type AppDispatch =
  typeof store.dispatch

export type AppStore =
  typeof store

hooks.ts

Das Hooks-Modul wandelt diese Typen in Anwendungs-Hooks um:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Eine Komponente, die beides verwendet

Eine Zählerkomponente liest und schreibt anschließend ausschließlich über die typisierten Hooks:

function Counter() {
  const count = useAppSelector(
    state => state.counter.value
  )

const dispatch = useAppDispatch()
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() =>
          dispatch(increment())
        }
      >
        +
      </button>
    </>
  )
}

Der Ablauf in dieser Komponente entspricht genau dem Kernzyklus von Anfang an:

Component
    │
    ├── useAppSelector()
    │        ↓
    │      READ
    │
    └── useAppDispatch()
             ↓
          DISPATCH
             ↓
           Redux
             ↓
          New State
             ↓
       useAppSelector()
             ↓
          Component

Wo Redux Toolkit passt

Redux Toolkit und React-Redux ergänzen sich, sind aber keine Alternativen:

Redux Toolkit
      +
React-Redux

Redux Toolkit verbessert den Redux-Teil: Einrichtung des Stores, Reducer, asynchrone Logik, gememorierte Selektoren sowie das Abrufen von Daten.

configureStore
createSlice
createAsyncThunk
createSelector
RTK Query

React-Redux ist weiterhin für die Verbindung zu React verantwortlich:

Provider
useSelector
useDispatch
useStore
connect

Die offizielle React-Redux Quick Start-Einrichtung stellt beide zusammen bereit, und diese Kombination ist die Standardlösung für neue Projekte.

Der gesamte Ablauf in einem Diagramm

Alle Komponenten zusammengefasst, vom Provider bis zur Gleichheitsprüfung:

React
                   │
                   ▼
             <Provider>
                   │
                   ▼
             Redux Store
                   │
          ┌────────┴────────┐
          │                 │
    useSelector()      useDispatch()
          │                 │
          │                 ▼
          │               Action
          │                 │
          │                 ▼
          │              Reducer
          │                 │
          │                 ▼
          │             New State
          │                 │
          └─────────┬───────┘
                    ▼
              Selector runs
                    │
                    ▼
             Equality check
                    │
             ┌──────┴──────┐
             │             │
           Same         Different
             │             │
             ▼             ▼
         No Redux       Re-render
          render

Häufige Fehler und wie man sie erkennt

Abonnieren des gesamten States

useSelector(state => state)

Ersetzen Sie dies durch spezifische Selektoren.

Werte in ein neues Objekt einpacken

useSelector(state => ({
  user: state.user
}))

Teilen Sie dies in separate Hooks auf oder fügen Sie absichtlich shallowEqual hinzu.

Filtern oder Zuordnen innerhalb des Selectors bei jedem Ausführungsvorgang

useSelector(state =>
  state.todos.filter(...)
)

Falls dies häufig oder bei großen Listen ausgeführt wird, sollte es in einen memoisierten Selector verlegt werden.

Lesen von Render-Daten über useStore

Durch Aufruf von

store.getState()

in der Render-Logik erhält man einen Wert ohne Abonnement. Verwenden Sie stattdessen

useSelector()

damit die Komponente aktualisiert wird, wenn sich der Wert ändert.

Direkte Änderung des Zustands

Eine Zuweisung wie

state.user.name = 'John'

bricht das immutable Update-Modell von Redux: Referenzen ändern sich nicht, wodurch Selectors „keine Änderung“ erkennen und Komponenten sich nicht rendern. (In den createSlice-Reducers von Redux Toolkit ist dieser Ansatz zulässig, da Immer ihn in eine immutable Änderung umwandelt; an allen anderen Stellen handelt es sich um einen Fehler.)

Nebeneffekte innerhalb von Selectoren

state => {
  fetch(...)
  return state.user
}

Ein Selector sollte nur berechnen und zurückgeben, nichts anderes.

Reflexives Memoisieren

Das Überall-Einschmuggeln davon

useMemo()
useCallback()
React.memo()

führt zu zusätzlicher Komplexität und Vergleichskosten, ohne dass nachgewiesen werden kann, dass es hilft. Optimieren Sie die bereits gemessene Darstellung.

Falsche Platzierung von memoisierten Selektor-Instanzen

Ein mit Cache ausgestatteter Selektor verhält sich je nach seinem Scope unterschiedlich. Bevor Sie ihn verwenden, sollten Sie wissen, ob er:

global
per component
per component instance

Eine gemeinsam genutzte Instanz, die mit vielen verschiedenen Argumenten verwendet wird, erreicht möglicherweise nie ihren Cache.

Interviewfragen mit kurzen Antworten

Was ist React-Redux und warum ist der Provider notwendig?

Es handelt sich um die offizielle React-Bindung für Redux: Provider, die Hooks sowie connect ermöglichen es Komponenten, den Zustand zu lesen, auf Änderungen zu reagieren und Aktionen abzusenden. Der Provider platziert den Store in den React-Context, sodass alle Nachkommen darauf zugreifen können.

useSelector gegenüber useDispatch?

Eines dient zum Lesen:

useSelector
    ↓
READ Redux state

Die andere Schreibweise:

useDispatch
    ↓
DISPATCH Redux actions

Wie löst useSelector eine Neuansicht aus?

Nach jeder Aktion wird der Selektor erneut ausgeführt und das Ergebnis mit dem vorherigen mithilfe von === oder der übergebenen Gleichheitsfunktion verglichen. Nur bei einem Unterschied wird eine Neuansicht ausgelöst.

Warum führt dieser Selektor ständig zu einer Neuansicht, und wie behebt man das?

useSelector(state => ({
  user: state.user,
  count: state.count
}))

Er erstellt bei jedem Ausführungsvorgang ein neues Objekt, wodurch die Referenzprüfung stets fehlschlägt. Drei Lösungsansätze:

1. Multiple useSelector calls

2. shallowEqual

3. Memoized selector

useSelector gegenüber connect?

Hooks vergleichen die Ergebnisse des Selektors anhand der Referenz; connect() führt einen oberflächlichen Vergleich der Props aus mapStateToProps durch. Hooks sind die Standardlösung, connect() bleibt weiterhin unterstützt.

Warum memoisierte Selektoren?

Sie berechnen abgeleitete Daten nur dann erneut, wenn sich die Eingaben ändern, ansonsten geben sie denselben Referenzwert zurück. Ein Filter ist ein klassisches Beispiel dafür:

todos
  ↓
filter completed
  ↓
new array

Was sind RootState, AppDispatch und withTypes()?

Die abgeleitete Typisierung des gesamten Zustands:

type RootState =
  ReturnType<typeof store.getState>

Die abgeleitete Typisierung für Dispatch, einschließlich Middleware wie Thunks:

type AppDispatch =
  typeof store.dispatch

Und die Hilfsfunktionen, die Hooks zurückgeben, die bereits mit diesen Typen vorauskonfiguriert sind:

useDispatch.withTypes<AppDispatch>()

useSelector.withTypes<RootState>()

useStore.withTypes<AppStore>()

Warum typisierte Hooks?

Sie ersetzen wiederholte Annotationen wie

useSelector(
  (state: RootState) =>
    state.user
)

durch

useAppSelector(
  state => state.user
)

Warum müssen Selektoren rein sein?

Ein Selektor kann mehrmals für denselben Zustand ausgeführt werden – während des Renderings, nach jeder Aktion sowie zu Zeiten, die Ihr Code nicht kontrolliert. Jeder Nebeneffekt würde somit eine unvorhersehbare Anzahl an Malen ausgeführt werden.

Wofür dient useStore?

Seltene Fälle, in denen tatsächlich das Store-Objekt benötigt wird. Das Lesen des Zustands zur Darstellung erfolgt über useSelector().

Was sind Zombie-Children und veraltete Props?

Sowohl sind seltene Probleme bezüglich der Reihenfolge. Ein Zombie-Child verarbeitet eine Aktualisierung, bevor sein Elternteil es deinstalliert, und liest dabei gelöschte Daten; veraltete Props bedeuten, dass ein auf Props angewiesener Selector mit neuem Zustand, aber alten Props ausgeführt wird. Defensive Selectors kümmern sich um beides.

Ist batch() in React 18 noch notwendig?

In der Regel nicht, da React 18 automatisch in Batches arbeitet, aber man trifft es immer noch in älterem Code an.

Warum können connect und Hooks unterschiedlich rendern?

Andere Abonnementsmodelle sowie verschiedene Vergleiche:

connect()
    ↓
shallow equality

gegenüber

useSelector()
    ↓
strict === equality

Warum wird useSelector so oft ausgeführt?

Es:

  1. wird während der Renderung ausgeführt
  2. horcht auf den Store
  • wird nach jeder versendeten Aktion erneut ausgeführt
  • wiederverwendet sein in Cache gespeichertes Ergebnis während der Darstellung nur, wenn die Selektorfunktion und der Zustand unverändert sind
  • Daher wird ein inline-Selektor, da er bei jeder Darstellung eine neue Funktion ist, bei jeder Darstellung ausgeführt; eine stabile Selektorenreferenz ermöglicht es React-Redux, diesen Aufruf zu überspringen.

    Was in welcher Reihenfolge studiert werden sollte

    Niveau eins: muss auswendig gekannt werden

    Provider
    useSelector
    useDispatch
    Redux Store flow
    Selectors
    === equality
    Re-render behavior
    Redux Toolkit + React-Redux
    TypeScript
    RootState
    AppDispatch
    .withTypes()
    

    Niveau zwei: solides praktisches Wissen

    shallowEqual
    Memoized selectors
    createSelector
    useStore
    connect
    mapStateToProps
    mapDispatchToProps
    ConnectedProps
    React.memo
    

    Niveau drei: fortgeschrittene Themen

    Stale props
    Zombie children
    Selector + props
    Selector memoization
    Custom context
    Development mode checks
    SSR serverState
    batch()
    

    Was man nicht auswendig lernen muss

    Es ist nicht notwendig, die Dokumentation Zeile für Zeile zu lernen. Es ist in Ordnung, Folgendes auszulassen:

    • wie das Abonnementsmechanismus intern implementiert ist
    • seltener verwendete connect()-Optionen
    • seit Langem veraltete Muster
    • Implementierungsdetails des Quellcodes
  • der genaue Text jeder Entwicklungswarnung
  • jede fortgeschrittene Provider-Eigenschaft
  • Wichtig ist die Begründung hinter den Regeln. Das Wissen um diese Tatsache

    useSelector uses ===
    

    ist weniger nützlich als die Fähigkeit, antworten zu können

    Why?
    

    nämlich:

    Because returning a new object
    creates a new reference.
    

    was wiederum zu einer Kette führt

    New reference
         ↓
    === false
         ↓
    re-render
    

    Wenn Sie diese Kette verstehen, können Sie den größten Teil der anderen Regeln selbst ableiten.

    Zehn Regeln, die man befolgen sollte

    1. Mit useSelector lesen

    useSelector()
    

    so lesen Komponenten den Zustand.

    2. Mit useDispatch schreiben

    useDispatch()
    

    so senden Komponenten Aktionen.

    3. Die App in Provider einpacken

    <Provider store={store}>
    

    4. Die Vergleichsregeln im Gedächtnis behalten

    useSelector → ===
    connect → shallow comparison
    

    5. Den Wurzelzustand niemals auswählen

    useSelector(state => state)
    

    6. Vorsichtig mit Selektoren sein, die Objekte zurückgeben

    useSelector(state => ({
      ...
    }))
    

    7. Teure abgeleitete Daten merken

    Memoized selector
    

    8. Selektoren diszipliniert verwenden

    Pure
    Predictable
    Granular
    

    9. Zuerst den Store, dann die Hooks eingeben

    Zuerst die abgeleiteten Typen:

    RootState
    AppDispatch
    AppStore
    

    dann die daraus erstellten Anwendungs-Hooks:

    useAppSelector
    useAppDispatch
    useAppStore
    

    10. Die moderne Kombination nutzen

    Redux Toolkit
           +
    React-Redux Hooks
    

    Die gesamte API auf einer Seite

    Eine kompakte Referenz, die die Kern-Hooks, Leistungsstrategien, TypeScript-Typen, die Legacy-API sowie fortgeschrittene Themen umfasst:

    ┌───────────────────────────────────────────────┐
    │               REACT-REDUX                     │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ <Provider store={store}>                      │
    │        ↓                                      │
    │ Makes Redux available to React                │
    │                                               │
    │ useSelector()                                 │
    │        ↓                                      │
    │ READ state                                    │
    │        ↓                                      │
    │ Default comparison: ===                       │
    │                                               │
    │ useDispatch()                                 │
    │        ↓                                      │
    │ DISPATCH actions                              │
    │                                               │
    │ useStore()                                    │
    │        ↓                                      │
    │ Direct store access                           │
    │        ↓                                      │
    │ Use rarely                                    │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ PERFORMANCE                                   │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ Avoid state => state                          │
    │ Avoid unnecessary object creation             │
    │ Use granular selectors                        │
    │ Use shallowEqual when appropriate             │
    │ Use memoized selectors for derived data       │
    │ Use React.memo when justified                 │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ TYPESCRIPT                                    │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ RootState = ReturnType<typeof store.getState> │
    │ AppDispatch = typeof store.dispatch           │
    │ AppStore = typeof store                       │
    │                                               │
    │ useAppSelector                                │
    │ useAppDispatch                                │
    │ useAppStore                                   │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ LEGACY / EXISTING APPLICATIONS                │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ connect()                                     │
    │ mapStateToProps                               │
    │ mapDispatchToProps                            │
    │ ConnectedProps                                │
    │                                               │
    ├───────────────────────────────────────────────┤
    │ ADVANCED                                      │
    ├───────────────────────────────────────────────┤
    │                                               │
    │ Stale Props                                   │
    │ Zombie Children                               │
    │ Custom Context                                │
    │ SSR serverState                               │
    │ Development checks                            │
    │ batch()                                       │
    │                                               │
    └───────────────────────────────────────────────┘
    

    Und der Zyklus, erneut dargestellt mit den Lese- und Schreibpfaden nebeneinander:

                            REACT
                               │
                               │
                        <Provider />
                               │
                               ▼
                        ┌─────────────┐
                        │ Redux Store │
                        └──────┬──────┘
                               │
                   ┌───────────┴───────────┐
                   │                       │
                   ▼                       ▲
            useSelector()             useDispatch()
                   │                       │
                   │                       │
                 READ                    ACTION
                   │                       │
                   │                       │
                   │                  ┌────┴─────┐
                   │                  │ Reducer  │
                   │                  └────┬─────┘
                   │                       │
                   │                       ▼
                   │                  New State
                   │                       │
                   └───────────────────────┘
                               │
                               ▼
                        Equality Check
                               │
                        ┌──────┴──────┐
                        │             │
                      Same         Changed
                        │             │
                        ▼             ▼
                    No Redux      Re-render
                     render
    

    Für ein neues TypeScript-Projekt reduziert sich die empfohlene Einrichtung auf diese Struktur:

                Redux Toolkit
                       +
                  React-Redux
                       │
            ┌──────────┴──────────┐
            │                     │
         Store                 Provider
            │                     │
            │               Application tree
            │                     │
            └──────────┬──────────┘
                       │
              ┌────────┴────────┐
              │                 │
       useAppSelector()   useAppDispatch()
              │                 │
            READ              WRITE
              │                 │
              └────────┬────────┘
                       │
                    Redux
                       │
                  New State
                       │
                    Selector
                       │
                    Re-render
    

    Zusammenfassung

    React-Redux wird viel einfacher, sobald man seine Exporte nicht mehr als unverbundene Tools betrachtet. Diese Liste von Namen

    Provider
    useSelector
    useDispatch
    connect
    shallowEqual
    createSelector
    useStore
    

    Beschreibt eine einzige Pipeline mit einer Aufgabe pro Phase. Der Anbieter stellt den Speicher zur Verfügung:

    Provider
       ↓
    makes Store available
    

    Der Selector-Hook liest ein:

    useSelector
       ↓
    reads selected state
    

    Der Dispatch-Hook sendet aus:

    useDispatch
       ↓
    sends actions
    

    Reducer erzeugen den nächsten Zustand:

    Reducer
       ↓
    creates new state
    

    Selector formen diesen Zustand für die Benutzeroberfläche:

    Selector
       ↓
    derives data
    

    Die Gleichheitsprüfung entscheidet, ob sich etwas Relevantes geändert hat:

    Equality
       ↓
    decides whether selected data changed
    

    Und React übernimmt die Darstellung:

    React
       ↓
    re-renders when necessary
    

    Auf der TypeScript-Seite ist die Abfolge genauso linear:

    Store
     ↓
    RootState
    AppDispatch
    AppStore
     ↓
    .withTypes()
     ↓
    useAppSelector()
    useAppDispatch()
    useAppStore()
    

    Wenn ein Komponente mehr darstellt, als sie sollte, muss man jedes Mal die gleichen vier Fragen beantworten:

    useSelector()
          ↓
    What does my selector return?
          ↓
    Is the reference stable?
          ↓
    Does the selected value actually change?
          ↓
    Should this component re-render?
    

    Wichtige Erkenntnisse:

    • Die meisten Leistungsprobleme bei React-Redux entstehen durch Selector, die neue Referenzen zurückgeben, nicht durch Redux selbst.
  • useSelector() vergleicht mit ===; connect() führt einen oberflächlichen Vergleich durch. Das Portieren von Code zwischen diesen beiden ohne Anpassung der Selektoren verändert das Verhalten.
  • Wählen Sie selektiv, merken Sie abgeleitete Daten mit createSelector-Instanzen auf Modulebene ab und verwenden Sie shallowEqual nur dann, wenn ein Objektergebnis beabsichtigt ist.
  • Ermitteln Sie RootState, AppDispatch und AppStore aus dem Store und erstellen Sie typisierte Hooks mit .withTypes().
  • Veraltete Props, „Zombie“-Kinderkomponenten, benutzerdefinierte Kontexte, batch() und serverState sind zwar verständniswert, gehören aber zu Randfällen und nicht zu alltäglichen Problemen.
  • Als Selbsttest versuchen Sie, jeden Abschnitt dieses Leitfadens aus dem Gedächtnis zu erklären – von Provider und Gleichheit über typbasierte Hooks bis hin zu serverState – und die grundlegende Einrichtung ohne Nachschlagen zu erstellen.

    Zu den detaillierten Informationen dienen die offiziellen Referenzen: die Hooks-API, die Provider-API, der TypeScript-Nutzungshinweis, das Schnellstart sowie die connect()-Referenz. Studieren Sie zunächst den Kernloop und die Gleichheitsprüfung, danach Selektoren, TypeScript, Leistung, connect sowie die Randfälle, damit die API-Details einen Bezugspunkt haben.

    Zusätzliche Literatur

  • Prop Drilling Ist Kein Grund, Redux oder Zustand Zu Installieren — Testen Sie vier gängige Gründe für den Einsatz einer State-Bibliothek anhand funktionierenden React-Code: Context für Prop Drilling, useState, useSyncExternalStore sowie die Kosten des Neuberechnens durch Context.
  • Reacts Scheduler und der Event Loop: Wer Entscheidet Tatsächlich, Wann Arbeiten Ausgeführt Werden — Erfahren Sie, wie Reacts kooperativer Scheduler innerhalb des JavaScript-Event Loops läuft, warum Übergänge warten können und warum kein Scheduler einen blockierten Thread retten kann.
  • Über React Hooks durch Render-Snaps und Kompromisse nachdenken — Ein fortgeschrittenes Denkmodell für React- und React-Native-Hooks: Render-Snaps, Effects, Refs, Memoisierung, benutzerdefinierte Hooks sowie wie man in Interviews Kompromisse erklärt.