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.
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:
- liest den aktuellen Store-Zustand
- ruft Ihren Selektor damit auf
- speichert den zurückgegebenen Wert als „letztes ausgewähltes“ Ergebnis
- registriert eine Abonnierung am Store für dieses Komponente
- führt den Selektor nach jeder gesendeten Aktion erneut aus
- vergleicht das vorherige Ergebnis mit dem neuen
- 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
dispatchakzeptiert - 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:
- nur die Daten zurückgeben, die die Komponente verwendet
- wenn sich nichts Relevantes geändert hat, stabile Referenzen zurückgeben
- ohne Not neue Objekte oder Arrays zu erstellen
- 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:
- Das Kind meldet sich beim Store an.
- Eine Aktion löscht die Daten, die das Kind anzeigt.
- Der Elternteil wird bei der nächsten Darstellung dieses Kindes nicht mehr darstellen.
- Vorher läuft jedoch noch die Anmeldung des Kindes ab.
- 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:
- wird während der Renderung ausgeführt
- horcht auf den Store
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
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.createSelector-Instanzen auf Modulebene ab und verwenden Sie shallowEqual nur dann, wenn ein Objektergebnis beabsichtigt ist.RootState, AppDispatch und AppStore aus dem Store und erstellen Sie typisierte Hooks mit .withTypes().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
- React Query und Redux: Die Server-State in großen Anwendungen neu überdenken — Erfahren Sie, warum eine Produktions-Chat-Anwendung TanStack Query anstelle von Redux zur Verwaltung von Serverdaten verwendet hat, und wo Redux in der modernen React-Architektur weiterhin seinen Platz findet.
- Strukturierung einer TanStack Query-Datenlage, von queryOptions bis Rollbacks — Erstellen Sie schrittweise eine TanStack Query-Datenlage: gemeinsame queryOptions, Schlüsselgeneratoren, Selektoren, Paginierung, Vorausladeung, zentrale Invalidation sowie sichere optimistische Updates.