Warum solltest du Redux im Jahr 2024 und darüber hinaus meistern wollen?

Ich habe Redux-Anwendungen entwickelt, die auf Hunderttausende gleichzeitiger Nutzer skaliert wurden. Und in diesem Artikel werde ich die Lektionen mit dir teilen, die ich gelernt habe.
Wenn du Anfänger bist, wird dir dieser Artikel Redux von Grund auf beibringen.
Und selbst wenn du Redux schon seit Jahren verwendest, wird dieser Artikel alle Wissenslücken schließen und geheime Tricks verraten, um den saubersten Code zu schreiben, der möglich ist.
Dieser Artikel ist der erste Teil einer fünfteiligen Serie über Redux. Die ersten drei zusammen werden dir ein tieferes Verständnis vermitteln als 98 % des Marktes. Und die letzten beiden sind Tutorials, in denen du produktionsreife Redux-Anwendungen erstellst.
Die meisten Redux-Entwickler beginnen oft mit zwei zentralen Fragen:
Die Antwort auf diese Fragen wirst du am Ende dieses Artikels erhalten, denn um die Antworten vollständig zu verstehen, musst du Redux verstehen.
Stattdessen beginnen wir mit der Beantwortung der Frage ...
... und wofür ist Redux?
Redux ist eine JavaScript-Bibliothek zur Verwaltung des Anwendungszustands. Redux erleichtert die Handhabung komplexer Zustände in großen Anwendungen, indem es einen einzigen, globalen Store verwendet, der den Anwendungszustand enthält.
Der Name „Redux“ leitet sich von der Array-Methode „reduce“ in Kombination mit der „Flux“-Architektur ab.
Der „reduce“ -Teil bezieht sich auf die Reducer-Funktionen in Redux, die Änderungen am Zustand Ihrer Anwendung verwalten.
Das Design von Redux ist stark von vielen Technologien beeinflusst, aber die wichtigste ist die Elm-Architektur:
Darauf deutet der „Flux“-Teil in Redux hin, da die Flux-Architektur im Grunde dieselbe ist.
Redux besteht aus 6 Bausteinen:
In Redux, the Flux architecture can be translated as follows:
Store,This diagram shows you the data flow in Redux and how these 6 components work together.

Ihre React-Komponenten haben Zugriff auf den Store Ihrer Anwendung dispatch -Methode. Wie das funktioniert, erfahren Sie später in diesem Artikel.
Sie dispatchen dann eine Aktion, die an Ihre Middleware zur Verarbeitung und zur Handhabung von Side Effects weitergeleitet wird, bevor sie Ihre Reducer erreicht.
Ihre Reducer aktualisieren den globalen Anwendungsstatus.
Der aktualisierte Status fließt über Ihre Selektoren zurück in Ihre React-Komponenten und löst ein erneutes Rendern aus.
Dieser Artikel enthält viele Codebeispiele, und ich empfehle Ihnen dringend, mitzuprogrammieren, da Sie so das meiste Wissen behalten werden.
Erstellen Sie also ein neues Next.js 15-Projekt.
npx create-next-app@latestKonfigurieren Sie dann Ihr Projekt, indem Sie Ja bei allem außer TypeScript auswählen. Der dritte Artikel dieser Reihe behandelt Redux mit TypeScript.
✔ **What is your project named?** … redux-mastery-part-one
✔ **Would you like to use** **TypeScript****?** … No / Yes
No
✔ **Would you like to use** **ESLint****?** … No / Yes
Yes
✔ **Would you like to use** **Tailwind CSS****?** … No / Yes
Yes
✔ **Would you like to use** **`src/` directory****?** … No / Yes
Yes
✔ **Would you like to use** **App Router****? (recommended)** … No / Yes
Yes
✔ **Would you like to customize the default** **import alias** **(@/*)?** … No / No.reduce()Sie sollten die Grundlagen von JavaScript kennen, wie zum Beispiel, was ein Objekt ist, was Funktionen sind und was die .reduce() Array-Methode ist.
Zur Auffrischung: Die .reduce() -Methode in JavaScript verarbeitet jedes Element in einem Array. Sie kombiniert diese zu einem einzigen Ausgabewert. Die Methode nimmt eine Funktion als Argument entgegen. Diese Funktion wird als Reducer Funktion und auf jedes Element im Array angewendet.
Hier ist ein einfaches Beispiel, das .reduce() verwendet, um ein Array von Zahlen zu summieren:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((total, current) => total + current, 0);
console.log(sum); // Output: 10Jede Reducer-Funktion nimmt zwei Parameter entgegen: den Akkumulator, in diesem Fall genannt total und den aktuellen Wert.
Falls das zu schnell oder zu viel für Sie war, lesen Sie "Entfesseln Sie das Potenzial von JavaScript mit funktionaler Programmierung" das all diese Dinge ausführlich erklärt. Es bereitet Sie auch auf eine fortgeschrittene Selektorkomposition vor, die Sie später in diesem Artikel lernen werden.
Lassen Sie uns nun alle Bausteine von Redux aufschlüsseln. Beginnend mit ...
Aktionen sind die einzige Möglichkeit, wie Ihre Anwendung mit dem Store in Redux kommuniziert.
const actionWithoutPayload = { type: "some-string" };
const actionWithPayload = {
type: 'some-string',
payload: { message: "I can be anything. In this case, I'm an object." },
};Aktionen sind einfache JavaScript-Objekte und müssen eine type -Eigenschaft haben. Der type gibt den Zweck der Aktion an.
Sie können auch zusätzliche Daten über die Payload Eigenschaft. Ein Payload kann jeder serialisierbare Datentyp in JavaScript sein.
const saveUserAction = {
type: "saveUser",
payload: { id: 1, name: "Jan Hesters", job: "mentor" },
};saveUserAction zeigt ein praxisnäheres Beispiel. Eine solche Aktion könnte man sehen, wenn jemand eine Entität speichert, in diesem Fall einen Benutzer. Sie enthält den zu speichernden Benutzer als Payload.
Der zweite Baustein sind Reducer.
Reducer sind reine Funktionen in Redux, die den aktuellen Zustand und eine Aktion entgegennehmen und einen neuen Zustand zurückgeben.
Erstellen Sie eine Datei in src/app/example-reducer.js.
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT": {
return state + 1;
}
case "INCREMENT_BY": {
return state + action.payload;
}
default:
return state;
}
}state ist der Akkumulator, und action ist der aktuelle Wert. Wenn Sie Reducer schreiben, verwenden Sie normalerweise eine switch Anweisung, die den typeder Aktion auswertet. In diesem Beispiel ist der Reducer Funktion verarbeitet zwei Aktionstypen:
"INCREMENT" erhöht den Zustand um eins."INCREMENT_BY" erhöht den Zustand um den Betrag, der im Payload.Der Standard- Fall gibt den aktuellen Zustand unverändert zurück, was wichtig ist, um unbekannte Aktionen zu verarbeiten.
Denken Sie daran, Reducer müssen reine Funktionen sein – das bedeutet:
Date.now() oder Math.random().Stellen Sie außerdem sicher, dass jeder Fall die return Anweisung verwendet, um zu verhindern, dass ein Fall durchfällt.
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT": {
return state + 1;
}
case "INCREMENT_BY": {
return state + action.payload;
}
default:
return state;
}
}
const actions = [
{ type: "INCREMENT" },
{ type: "INCREMENT_BY", payload: 8958 },
{ type: "INCREMENT_BY", payload: 41 },
{ type: "INCREMENT" }
];
const state = actions.reduce(reducer, 0);
console.log('state', state); // state 9001Um Ihren Reducer zu verwenden, können Sie ein Array von Aktionen erstellen, das die zu verarbeitenden Aktionen auflistet. Beachten Sie, dass nur die 'INCREMENT_BY' Aktion eine Payload hat.
Jetzt können Sie Ihre Aktionen reduzieren, indem Sie Ihren Reducer als Reducer-Funktion und 0 als Anfangswert für den State. Das Ergebnis ist 9001. Dieses Anwendungsbeispiel veranschaulicht, woher der Reducer Teil des Namens in Redux stammt.
Sie können diesen Code refaktorisieren, um ihn sauberer zu gestalten.
const initialState = 0;
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT": {
return state + 1;
}
case "INCREMENT_BY": {
return state + action.payload;
}
case "RESET": {
return initialState;
}
default:
return state;
}
}
const actions = [
{ type: "INCREMENT" },
{ type: "INCREMENT_BY", payload: 8958 },
{ type: "RESET" },
{ type: "INCREMENT_BY", payload: 41 },
{ type: "INCREMENT" }
];
const state = actions.reduce(reducer, initialState);
console.log('state', state); // state 42Erfassen Sie den Anfangszustand in einer eigenen Variable, um den Code zu verbessern.
Dadurch können Sie einfach Standardparameter für den Anfangszustand verwenden und eine neue Aktion hinzufügen, um den Reducer auf seinen Anfangszustand zurückzusetzen.
Jeder Fall des Reducers muss Daten mit demselben Typ und derselben Struktur wie der Anfangszustand zurückgeben. Wenn der Anfangszustand eine Zahl ist, muss jeder Fall eine Zahl zurückgeben. Wenn der Anfangszustand ein Objekt mit bestimmten Eigenschaften ist, muss jeder Fall ein Objekt derselben Struktur zurückgeben.
Sie können es testen, indem Sie es Ihrem Actions-Array hinzufügen. Und anstatt 0 fest zu codieren, können Sie den Anfangszustand an die reduce Methode übergeben. Dies verhindert Fehler, falls Sie den Anfangszustand jemals ändern. Das Ergebnis ist nun 42.
Sie können den Code aber noch weiter verbessern.
const increment = () => ({ type: "INCREMENT" });
const incrementBy = payload => ({ type: "INCREMENT_BY", payload });
const reset = () => ({ type: "RESET" });
const initialState = 0;
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT": {
return state + 1;
}
case "INCREMENT_BY": {
return state + action.payload;
}
case "RESET": {
return initialState;
}
default:
return state;
}
}
const actions = [
increment(),
incrementBy(8958),
reset(),
incrementBy(41),
increment()
];
const state = actions.reduce(reducer, initialState);
console.log('state', state); // state 42Erstellen Sie Fabrikfunktionen für Ihre Aktionen. In Redux werden diese als Action Creatorsbezeichnet. Wenn eine Aktion eine Payload benötigt, übergeben Sie diese als Parameter an den Action Creator.
Ersetzen Sie die fest codierten Aktionen in Ihrem Array durch Aufrufe dieser Action Creators. Sie übergeben die Zahlen als Argumente an die incrementBy Action Creators, die eine Payload benötigen.
Nach diesen Refaktorisierungen sollte der resultierende State gleich bleiben.
Action-Creatoren bieten mehrere Vorteile:
// Default parameters
const addNeighbor = ({ fullName = "N/A", joinDate = new Date() } = {}) => ({
type: "addNeighbor",
payload: { fullName, joinDate }
});
const neighbor = addNeighbor().payload;
// Mapping values
const fetchedUser = ({ firstName, lastName }) => ({
type: 'fetchedUser',
payload: { name: `${firstName} ${lastName}` },
});Können Sie diesen Code noch weiter bereinigen?
Ja, das können Sie. Jetzt lernen Sie einige Profitipps zur Strukturierung Ihres Redux-Codes.
export const increment = () => ({ type: 'INCREMENT' });
export const incrementBy = payload => ({ type: 'INCREMENT_BY', payload });
export const reset = () => ({ type: 'RESET' });
export const slice = 'example';
const initialState = { count: 0 };
export const reducer = (state = initialState, { type, payload } = {}) => {
switch (type) {
case increment().type: {
return { ...state, count: state.count + 1 };
}
case incrementBy().type: {
return { ...state, count: state.count + payload };
}
case reset().type: {
return initialState;
}
default: {
return state;
}
}
};
const actions = [
increment(),
incrementBy(8958),
reset(),
incrementBy(41),
increment(),
];
const state = actions.reduce(reducer, reducer());
console.log('state', state); // state { count: 42 }Zuerst erstellen Sie eine neue Variable namens slice. Die slice Variable ist eine Fehlbezeichnung. Ein besserer Name wäre sliceName oder substateName.
Ein Slice bezieht sich normalerweise auf den Unterzustand des Root-Zustands, wie ein Stück Kuchen. Jeder Slice ist für eine bestimmte Funktion oder einen Bereich innerhalb Ihrer App verantwortlich. Er umfasst einen eigenen Reducer, Action Creators und Selektoren. In diesem Fall ist der Slice der Beispiel-Unterzustand. Sie werden später in diesem Artikel genau erfahren, wie die Slice-Variable verwendet wird. Das ist noch nichts Komplexes, sondern ein einfacher Schritt, der Ihren Code für die spätere korrekte Verwendung in Ihrer App vorbereitet.
Als Nächstes destrukturieren Sie den type und die payload Ihrer Action in Ihrem Reducer und weisen ihnen standardmäßig ein leeres Objekt zu. Destrukturierung reduziert die Menge an Code, die Sie in Ihrer switch-Anweisung schreiben müssen.
Koppeln Sie Ihre Action Creators auch direkt an Ihre Switch-Cases, indem Sie die .type-Eigenschaft der Action Creators verwenden, anstatt Strings fest zu codieren. Dieser Ansatz verhindert Tippfehler in Action Types und erleichtert Refactorings, da Änderungen in Ihren Action Types automatisch an Ihren Reducer weitergegeben werden.
Um Ihren Reducer als reine Funktion erstellen Sie neue Objekte mithilfe der Spread-Syntax, anstatt den aktuellen Zustand zu ändern. Ihre Reducer müssen immer den Zustand unveränderlich modifizieren. Wenn Sie Ihren Zustand unveränderlich halten, werden Ihre Zustandsaktualisierungen vorhersehbar und es hilft bei effizienten Rendering-Updates.
Der zweite Grund, Spread zu verwenden, ist, die Form Ihres Zustands intakt zu halten. In jedem Fall aktualisieren Sie nur die count Eigenschaft. Wenn Sie Ihrem Zustand weitere Eigenschaften hinzufügen, stellt das Spreading sicher, dass Sie diese nicht überschreiben und nur den Schlüssel ändern, den Sie ändern möchten.
Ihre Umgestaltung hat einen letzten Vorteil: Wenn Sie Ihren Reducer ohne Argumente aufrufen, gibt er nun den Anfangszustand zurück. Dies stellt sicher, dass Sie, wo immer Sie Ihren Reducer importieren und seinen Anfangszustand benötigen, den Anfangszustand nicht separat importieren müssen, was das Fehlerrisiko verringert.
Als Nächstes lernen Sie den dritten Baustein kennen: den Store. Der Store ist ein zentraler Ort, an dem sich der Zustand Ihrer Anwendung befindet und Reducer und Middleware miteinander verbunden sind.
So sieht eine Beispielimplementierung aus. (Erstellen Sie eine neue Datei namens src/app/create-store.js.)
export function createStore(reducer, initialState) {
let state = initialState;
const getState = () => state;
return { getState };
}Der Zustand Ihrer Anwendung wird in der Closure der createStore Funktion gekapselt.
In Ihrer createStore Implementierung, die getState Funktion verwendet Closures zur Kapselung und zum Datenschutz. Da getState innerhalb von createStoredefiniert ist, hat sie exklusiven Zugriff auf die state Variable, wodurch diese privat bleibt und manuelle Änderungen verhindert werden.
Erstellen Sie dann eine neue Datei namens src/app/store.js und importieren Sie Ihre createStore Funktion.
import { createStore } from './create-store';
import { reducer } from './example-reducer';
const store = createStore(reducer, reducer());
console.log('state', store.getState()); // state { count: 0 }Verwenden Sie Ihre createStore Funktion, um Ihren Store zu erstellen. Der Aufruf des reducer ohne Argumente liefert den initialen Zustand, der { count: 0 } in diesem Fall ist. Sie können getState() um auf den aktuellen Zustand Ihres Stores zuzugreifen.
Das Redux-Paket enthält eine eigene, ausgefeiltere Version der createStore Funktion, installieren Sie also das redux Paket.
npm i reduxImportieren Sie dann die createStore Funktion.
import { legacy_createStore as createStore } from 'redux';
import { reducer } from './example-reducer';
const store = createStore(reducer, reducer());
console.log('state', store.getState()); // state { count: 0 }Sie werden die legacy_createStore Import-Anweisung verwenden, da die reguläre createStore Import-Anweisung veraltet ist. Sie wird zwar weiterhin funktionieren, aber die Redux-Maintainer haben sie als veraltet markiert, um die Leute dazu zu bewegen, Redux Toolkit zu verwenden, das Sie im Artikel dieser Serie kennenlernen werden.
Nachdem Sie nun Ihren Store haben, wie senden Sie Aktionen an ihn, um seinen Zustand zu manipulieren? Dafür verwenden Sie eine Funktion namens dispatch dafür.
Gehen Sie zurück zu Ihrer eigenen Implementierung der createStore Funktion und modifizieren Sie sie, um eine dispatch Methode bereitzustellen.
export function createStore(reducer, initialState) {
let state = initialState;
const dispatch = action => {
state = reducer(state, action);
};
const getState = () => state;
return { dispatch, getState };
}dispatch nimmt eine Aktion entgegen und verwendet den reducer aus Ihrer createStore Funktion, um den nächsten Zustand zu berechnen, und mutiert dann den Zustand in der Closure.
Der Store, der mit der createStore Funktion von Redux erstellt wurde, verfügt bereits über eine dispatch Methode.
import { legacy_createStore as createStore } from 'redux';
import { increment, incrementBy, reducer, reset } from './example-reducer';
const store = createStore(reducer, reducer());
const actions = [
increment(),
incrementBy(5),
reset(),
incrementBy(41),
increment(),
];
for (const action of actions) {
store.dispatch(action);
}
console.log('state', store.getState()); // state { count: 42 }Definieren Sie ein Array von Aktionen unter Verwendung Ihrer Aktionen aus Ihrem Beispiel-Reducer. Verwenden Sie dann eine for -Schleife, um jede Aktion an den Store zu dispatchen. Danach liefert der Aufruf von getState() Ihnen den aktuellen Zustand Ihres Stores.
Sie haben gelernt, wie Sie Ihren Store mit einem einzigen Reducer einrichten. Doch wie bereits erwähnt, haben Redux-Anwendungen in der Regel viele Slices, die jeweils einer anderen Funktion oder einem anderen Bereich Ihrer Anwendung entsprechen. Wie gehen Sie also mit mehreren Reducern im selben Store um?
Um dies zu demonstrieren, erstellen Sie einen Benutzerprofil-Reducer in src/app/user-profile-reducer.js.
export const loginSucceeded = payload => ({ type: 'LOGIN_SUCCEEDED', payload });
export const usersFetched = payload => ({ type: 'USERS_FETCHED', payload });
export const slice = 'userProfile';
const initialState = { currentUserId: null, users: {} };
export const reducer = (state = initialState, { type, payload } = {}) => {
switch (type) {
case loginSucceeded().type: {
return {
...state,
currentUserId: payload.id,
users: { ...state.users, [payload.id]: payload },
};
}
case usersFetched().type: {
const newUsers = { ...state.users };
payload.forEach(user => {
newUsers[user.id] = user;
});
return { ...state, users: newUsers };
}
default: {
return state;
}
}
};Erstellen Sie zwei Aktionen: eine für die Benutzeranmeldung und eine weitere für den Abruf einer Benutzerliste.
Sie möchten auch Ihren State normalisieren. Das bedeutet, dass Sie, wenn ein Benutzer abgerufen wird, diesen in einem Objekt speichern werden, wobei der Schlüssel die Benutzer- idist und der Wert das gesamte Benutzerobjekt ist.
Eine befüllte State-Struktur für diesen Slice könnte so aussehen.
{
"currentUsersId": "abc-123",
"users": {
"abc-123": {
"id": "abc-123",
"email": "johndoe@example.com",
"fullName": "John Doe"
},
"xyz-789": {
"id": "xyz-789",
"email": "janesmith@example.com",
"fullName": "Jane Smith"
}
}
}Eine normalisierte State-Struktur bietet mehrere Vorteile gegenüber der Speicherung von Benutzern in einem Array:
Erstellen Sie immer ein neues Objekt, wenn Sie das Benutzerobjekt aktualisieren, um die Unveränderlichkeit des States zu gewährleisten.
Die Payload für den loginSucceeded Aktion enthält das vollständige Benutzerprofil des Benutzers, der sich gerade angemeldet hat, einschließlich dessen ID, Name und E-Mail-Adresse. Die Payload für die usersFetched Aktion ist ein Array von Benutzern.
Beachten Sie, dass die Action Creators und die Typen der von ihnen erstellten Aktionen im Präteritum benannt sind – dies ist eine gute Praxis und hat zwei Vorteile.
Erstens zeigt diese Namenskonvention an, welches Ereignis gerade stattgefunden hat, was das Debugging Ihrer App erleichtert. Manche Entwickler benennen ihre Aktionen wie einen Prozess, was das Debugging erschwert, da Ihnen das Wissen fehlt, wo in Ihrer App die Aktion ausgelöst wurde.
const setCurrentUser = payload => ({ type: 'SET_CURRENT_USER', payload }); // 🚫 Bad!
const loginSucceeded = payload => ({ type: 'LOGIN_SUCCEEDED', payload }); // ✅ Good!
const changedUser = payload => ({ type: 'CHANGED_USER', payload }); // ✅ Good!Zweitens, wenn zwei Aktionen dieselbe Statusaktualisierung auslösen, aber von unterschiedlichen Interaktionen stammen, helfen eindeutige Namen Ihnen zu erkennen, was passiert ist, sodass Sie wissen, wo in Ihrem Code Sie nach dem Fehler suchen müssen.
// 🚫 Bad! Avoid this 👇 It's only included in this example, so you see the anti-pattern.
const setCurrentUser = payload => ({ type: 'SET_CURRENT_USER', payload });
const loginSucceeded = payload => ({ type: 'LOGIN_SUCCEEDED', payload });
const changedUser = payload => ({ type: 'CHANGED_USER', payload });
const changeCurrentUser = (state, currentUser) => ({
...state,
currentUserId: currentUser.id,
users: { ...state.users, [currentUser.id]: currentUser },
});
export const reducer = (state = initialState, { type, payload } = {}) => {
switch (type) {
case setCurrentUser().type: { // 🚫 Bad! Only shown as an example.
return changeCurrentUser(state, payload);
}
case changedUser().type: {
return changeCurrentUser(state, payload);
}
case loginSucceeded().type: {
return changeCurrentUser(state, payload);
}
// ... rest
}
};Alle drei Aktionen lösen dieselbe Statusänderung aus, da Sie den aktuellen Benutzer aktualisieren müssen, nachdem er sich angemeldet hat, und es könnte auch eine Schnittstelle geben, die es dem Benutzer ermöglicht, Benutzer zu wechseln. Beide Fälle könnten durch die setCurrentUser Aktion. Wenn Sie jedoch sehen, dass die setCurrentUser Aktion ausgelöst wurde, haben Sie keine Ahnung, was gerade passiert ist. Aber da Sie ihnen eigene Namen gegeben haben, ist bei loginSucceeded und changeUser sofort offensichtlich, was passiert ist.
Wenn mehrere Aktionen dieselbe Statusaktualisierung erfordern, lagern Sie dies in eine Hilfsfunktion aus und verwenden Sie diese in Ihrem Reducer für mehrere Fälle. In diesem Fall heißt die Funktion changeCurrentUser.
Manche Entwickler verwenden Fall-Through in Switch-Anweisungen, um Aktualisierungen zu bündeln und dieselbe Statusaktualisierung zu verarbeiten. Sie sollten jedoch Fall-Through-Switch-Anweisungen vermeiden, um unerwünschte Fehler zu verhindern.
export const reducer = (state = initialState, { type, payload } = {}) => {
switch (type) {
// fall-through: bad 🚫
case setCurrentUser().type:
case changedUser().type:
// Imagine adding a case here with a new return statement. It would suddenly
// break the intended behavior of `setCurrentUser` and `changedUser`.
case loginSucceeded().type: {
return {
...state,
currentUserId: currentUser.id,
users: { ...state.users, [currentUser.id]: currentUser },
}
}
// ... rest
}
};Die Muster für die Handhabung von Switch-Anweisungen und Action Handlern sind nur ein Beispiel dafür, wie das Erlernen von Redux saubere Code-Muster fördert, die auf viele Programmierszenarien jenseits von Redux angewendet werden können.
Importieren Sie nun in Ihrem Store den Slice jedes Reducers neben dem entsprechenden Reducer.
import { legacy_createStore as createStore } from 'redux';
// Use `as` to avoid naming conflicts.
import { reducer as exampleReducer, slice as exampleSlice } from './example-reducer';
import {
reducer as userProfileReducer,
slice as userProfileSlice,
} from './user-profile-reducer';
function combineReducers(reducers) {
return function rootReducer(state = {}, action = {}) {
return Object.keys(reducers).reduce((nextState, slice) => {
nextState[slice] = reducers[slice](state[slice], action);
return nextState;
}, {});
};
}
const rootReducer = combineReducers({
[exampleSlice]: exampleReducer,
[userProfileSlice]: userProfileReducer,
});
const store = createStore(rootReducer, rootReducer());
console.log('state', store.getState());
// state {
// example: { count: 0 },
// userProfile: { currentUserId: null, users: {} }
// }Erstellen Sie dann eine combineReducers-Funktion. Die Funktion führt mehrere kleinere Reducer zu einem einzigen Reducer zusammen, der den gesamten Anwendungszustand verwaltet und üblicherweise als „Root-Reducer“ bezeichnet wird. Jeder Reducer, der an combineReducers übergeben wird, verwaltet seinen eigenen Teil des Zustands, der durch seinen jeweiligen Slice-Key definiert ist.
Wenn eine Aktion ausgelöst wird, combineReducers ruft jeden Reducer mit seinem aktuellen Slice des Zustands und der Aktion auf und führt dann die Ergebnisse zu einem neuen Zustandsobjekt zusammen.
Verwenden Sie Ihre combineReducers Funktion, um einen Root-Reducer zu erstellen und Ihren Store so anzupassen, dass er diesen Root-Reducer verwendet.
Wenn Sie getState()aufrufen, können Sie jeden Slice und seinen jeweiligen Zustand im aktuellen Zustand des Stores sehen.
Anstatt Ihre eigene combineReducers Funktion zu erstellen, verwenden Sie die combineReducers Funktion aus dem Redux-Paket.
import { combineReducers, legacy_createStore as createStore } from 'redux';
import { reducer as exampleReducer, slice as exampleSlice } from './example-reducer';
import {
reducer as userProfileReducer,
slice as userProfileSlice,
} from './user-profile-reducer';
const rootReducer = combineReducers({
[exampleSlice]: exampleReducer,
[userProfileSlice]: userProfileReducer,
});
const store = createStore(rootReducer, rootReducer());
console.log('state', store.getState());
// state {
// example: { count: 0 },
// userProfile: { currentUserId: null, users: {} }
// }Anstatt direkt aus Ihrem UI-Code mit Ihrem Store zu interagieren, wird Redux üblicherweise mit „UI-Binding“-Bibliotheken verwendet.
Für React gibt es React Redux, die offizielle Bibliothek, die vom Redux-Team gepflegt wird. React Redux verfügt über integrierte Leistungsoptimierungen, um sicherzustellen, dass Ihre Komponente nur bei Bedarf neu gerendert wird. Installieren Sie es in Ihrem Projekt.
npm i react-reduxIn Ihrer src/app/store.js, erstellen Sie eine makeStore Funktion und entfernen Sie Ihr store Objekt.
import { combineReducers, legacy_createStore as createStore } from 'redux';
import { reducer as exampleReducer, slice as exampleSlice } from './example-reducer';
import {
reducer as userProfileReducer,
slice as userProfileSlice,
} from './user-profile-reducer';
const rootReducer = combineReducers({
[exampleSlice]: exampleReducer,
[userProfileSlice]: userProfileReducer,
});
export const makeStore = () => {
return createStore(rootReducer, rootReducer());
};Ein Next.js-Server kann mehrere Anfragen gleichzeitig bearbeiten, daher müssen Sie für jede Anfrage einen neuen Redux-Store erstellen und vermeiden, den Store über Anfragen verschiedener Clients hinweg zu teilen. Deshalb erstellen Sie die makeStore Funktion, anstatt Ihr store als globale Variable. Wenn Sie Next.js NICHT mit dem app/ Verzeichnis verwenden, sondern an einer regulären Single Page Application (SPA) arbeiten, können Sie bedenkenlos eine globale store Variable. Aber in Next.js würde dies Probleme verursachen, da jeder Benutzer den gleichen Store-Referenz.
Erstellen Sie einen Provider für Ihren Store in src/app/store-provider.js und verwenden Sie die makeStore() Funktion darin.
'use client';
import { useRef } from 'react';
import { Provider } from 'react-redux';
import { makeStore } from './store';
export function StoreProvider({ children }) {
const storeRef = useRef();
if (!storeRef.current) {
storeRef.current = makeStore();
}
return <Provider store={storeRef.current}>{children}</Provider>;
}Importieren Sie den Provider aus React Redux und Ihre makeStore Funktion.
Verwenden Sie eine ref um sicherzustellen, dass der Store nur einmal erstellt wird. Obwohl die Komponente pro Serveranfrage nur einmal gerendert wird, kann sie auf dem Client mehrmals neu gerendert werden, wenn zustandsbehaftete Komponenten höher im Baum liegen oder die Komponente einen veränderlichen Zustand hat, der Neu-Renderings auslöst. Wenn sie neu gerendert wird, verhindern Sie die Erstellung eines neuen Stores durch die if Anweisung.
Übergeben Sie die ref Ihres Stores an den Provider aus React Redux.
Sie möchten Ihr StoreProvider überall im Komponentenbaum oberhalb wo der Store verwendet wird. In diesem Tutorial platzieren Sie Ihren Provider im Root-Layout, um Redux auf jeder Seite verfügbar zu machen. Dadurch wird der Redux-Store Ihrer gesamten App über die React Context APIverfügbar.
Ändern Sie Ihre Root-Layout-Datei in src/app/layout.js.
import './globals.css';
import { Inter } from 'next/font/google';
import { StoreProvider } from './store-provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Jan Hesters Redux Tutorial',
description: 'Part one of five to master Redux.',
};
export default function RootLayout({ children }) {
return (
<StoreProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</StoreProvider>
);
}Dadurch wird der Redux-Kontext Ihrer gesamten App zur Verfügung gestellt. Wenn Sie Redux nur auf bestimmten Routen verwenden möchten, können Sie den StoreProvider auf der jeweiligen Seite oder im Routen-Layout verwenden.
Wie erhalten Sie Daten aus dem Store in Ihre App? Hier kommen Selektoren ins Spiel.
Selektoren sind reine Funktionen, die Ihren Redux-Zustand entgegennehmen und einen bestimmten Teil davon oder einen aggregierten Wert zurückgeben.
Für die folgenden Beispiele müssen Sie nicht mitprogrammieren. Ich sage Ihnen Bescheid, wenn es wieder Zeit zum Mitprogrammieren ist.
import { combineReducers, legacy_createStore as createStore } from 'redux';
import {
incrementBy,
reducer as exampleReducer,
slice as exampleSlice,
} from './example-reducer';
import {
fetchedUsers,
loginSuccess,
reducer as userProfileReducer,
slice as userProfileSlice,
} from './user-profile-reducer';
const rootReducer = combineReducers({
[exampleSlice]: exampleReducer,
[userProfileSlice]: userProfileReducer,
});
export const makeStore = () => {
return createStore(rootReducer, rootReducer());
};
const store = makeStore();
store.dispatch(incrementBy(10));
store.dispatch(
loginSuccess({
id: 'user123',
email: 'johndoe@example.com',
firstName: 'John',
lastName: 'Doe',
}),
);
store.dispatch(
fetchedUsers([
{
id: 'user123',
email: 'johndoe@example.com',
firstName: 'John',
lastName: 'Doe',
},
{
id: 'user456',
email: 'janesmith@example.com',
firstName: 'Jane',
lastName: 'Smith',
},
]),
);
console.log('state', store.getState());
// state {
// "example": {
// "count": 10
// },
// "userProfile": {
// "currentUserId": "user123",
// "users": {
// "user123": {
// "id": "user123",
// "email": "johndoe@example.com",
// "firstName": "John",
// "lastName": "Doe",
// },
// "user456": {
// "id": "user456",
// "email": "janesmith@example.com",
// "firstName": "Jane",
// "lastName": "Smith",
// }
// }
// }
// }
const selectCurrentCount = state => state.example.count;
const currentCount = selectCurrentCount(store.getState());
console.log('currentCount', currentCount); // 10Denken Sie daran: Nachdem Sie Ihre Reducer mit Hilfe von combineReducers, können Sie Aktionen dispatchen, um Ihren Zustand zu ändern.
Das obige Codebeispiel zeigt, wie Aktionen ausgelöst werden, um einen Zustand mit einem Zählerstand von 10 und zwei Benutzern im Benutzerprofil-Slice zu erstellen. Ein Benutzer ist der aktuelle Benutzer, der in einer realen Anwendung über den Browser angemeldet wäre.
Sie können einen selectCurrentCount Selector schreiben, der den Zustand entgegennimmt und den aktuellen Zählerstand zurückgibt.
Und wenn Sie die E-Mail-Adresse des aktuellen Benutzers abrufen möchten, können Sie auch dafür einen Selector erstellen.
// ... existing code
const selectCurrentUsersEmail = state =>
state.userProfile.users[state.userProfile.currentUserId]?.email ?? '';
const currentUserEmail = selectCurrentUsersEmail(store.getState());
console.log('currentUserEmail', currentUserEmail); // johndoe@example.comSie können den normalisierten Zustand verwenden, um einfach auf das aktuelle Benutzerobjekt zuzugreifen und die E-Mail-Adresse mittels optionalem Eigenschaftszugriff abzurufen. Es ist bewährte Praxis, sicherzustellen, dass Ihr Selector immer denselben Datentyp mit aussagekräftigen Standardwerten zurückgibt. Wenn der Benutzer undefinedist, gibt der optionale Eigenschaftszugriff undefinedzurück, und der Nullish-Coalescing-Operator (??) gibt einen leeren String zurück. Dies stellt sicher, dass Ihr Selector immer einen String zurückgibt.
Selektoren können auch Argumente entgegennehmen und Werte aggregieren, wodurch sie neue Daten aus dem Zustand berechnen, filtern oder transformieren können.
Hier ist ein Beispiel für einen Selector, der sowohl eine Benutzer-ID als Parameter akzeptiert als auch den vollständigen Namen des Benutzers durch Kombination von Vor- und Nachnamen ableitet.
// ... existing code
const selectFullNameById = (state, userId) => {
const user = state.userProfile.users[userId];
return user ? `${user.firstName} ${user.lastName}` : 'User not found';
};
const user456FullName = selectFullNameById(store.getState(), 'user456');
console.log('user456FullName', user456FullName); // Jane Smith
const notFound = selectFullNameById(store.getState(), 'user789');
console.log('notFound', notFound); // User not foundWenn kein Benutzer gefunden wird, gibt er 'Benutzer nicht gefunden'zurück, was einen aussagekräftigen Standardwert liefert. Wenn Sie versuchen, die E-Mail-Adresse eines nicht existierenden Benutzers anzuzeigen, wird stattdessen "Benutzer nicht gefunden" angezeigt, was eine bessere Benutzererfahrung bietet, als nichts anzuzeigen oder Ihre App zum Absturz zu bringen.
Es gibt noch mehr über Selektoren zu lernen, aber der beste Weg, sie zu verstehen, ist im Kontext. Dazu müssen Sie sehen, wie React-Komponenten mit Redux verbunden werden.
React Redux bietet Ihnen zwei APIs, um Ihre Komponenten mit Redux zu verbinden. Sie können entweder Hooks oder ein HOC verwenden.
Zuerst sehen Sie die Hooks-API.
Jetzt können Sie wieder mitprogrammieren.
Erstellen Sie eine Datei src/app/hooks.js die die Redux-Hooks enthält.
import { useDispatch, useSelector, useStore } from 'react-redux';
export const useAppDispatch = useDispatch.withTypes();
export const useAppSelector = useSelector.withTypes();
export const useAppStore = useStore.withTypes();Wenn Sie Redux-Hooks verwenden möchten, nutzen Sie diese in Ihrer gesamten App anstelle der einfachen useDispatch und useSelector Hooks, da diese Hooks eine bessere Typsicherheit bieten.
Ändern Sie Ihre Haupt- page.js Komponente, um den Zähler aus Ihrem State abzurufen.
'use client';
import { useAppSelector } from './hooks';
export default function Home() {
const count = useAppSelector(state => state.example.count);
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h1 className="text-4xl font-bold">Count from the Example Slice</h1>
<div className="flex items-center justify-center space-x-4">
<p className="text-2xl">Count: {count}</p>
</div>
</main>
);
}Sie müssen die 'use client' Direktive auch hier verwenden, da nur Client-Komponenten Zugriff auf den Redux-Kontext haben.
Importieren Sie den useAppSelector Hook verwenden und die Selector-Funktion inline einfügen, um den aktuellen Zählerstand zu erhalten.
Denken Sie daran, Sie haben Ihr Root-Layout zuvor mit dem Redux-Provider umhüllt. Dadurch erhalten alle Komponenten im Komponentenbaum Zugriff auf den Redux-Kontext, der den Store enthält, Dispatchund den State. Der useAppSelector Hook hat Zugriff auf diesen Kontext und useAppSelector nimmt einen Selector als Callback entgegen. Anschließend übergibt er den State als erstes Argument an den Selector. Beachten Sie, dass Sie, da React Server Components keine Hooks oder Kontexte verwenden können, nicht aus dem Redux-Store innerhalb von RSCs lesen oder in diesen schreiben können.
Immer wenn der zurückgegebene Wert von useAppSelector sich ändert, wird die Home Komponente neu gerendert.
Es ist jedoch wichtig zu verstehen, dass useAppSelector seinen Wert automatisch memoisiert. Das bedeutet, dass ein Re-Render nur ausgelöst wird, wenn das Ergebnis des Selectors sich vom letzten Ergebnis unterscheidet, basierend auf einem strikten Gleichheitsvergleich (===) . Sie können sich "useCallback vs. useMemo" für eine detaillierte Erklärung, wie Memoization in React funktioniert. useAppSelector nutzt useCallback hinter den Kulissen, um mit React zu „sprechen“ und ein erneutes Rendern auszulösen.
useAppSelector nimmt einen Selektor als Callback-Funktion entgegen.
Denken Sie daran: Ein Selektor ist eine reine Funktion, die den globalen Zustand entgegennimmt und einen aggregierten oder abgeleiteten Wert zurückgibt.
Definieren Sie einen selectCurrentCount Selektor in Ihrer src/app/example-reducer.js unter Ihrem reducer.
// ... existing code
export const selectCurrentCount = state => state[slice].count;Selektoren in Variablen zu speichern ist besser, als sie inline zu definieren. Auf diese Weise müssen Sie, wenn Sie denselben Selektor in mehreren Komponenten verwenden und ihn ändern müssen, ihn nur einmal aktualisieren, anstatt jede Inline-Callback-Funktion in den Komponenten zu aktualisieren.
Verwenden Sie ihn nun in Ihrer Home Komponente und ersetzen Sie Ihre Inline-Funktion durch den Selektor.
'use client';
import { selectCurrentCount } from './example-reducer';
import { useAppSelector } from './hooks';
export default function Home() {
const count = useAppSelector(selectCurrentCount);
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h1 className="text-4xl font-bold">Redux Basics</h1>
<div className="flex items-center justify-center space-x-4">
<p className="text-2xl">Count: {count}</p>
</div>
</main>
);
}Sie werden bald mehr über die Vorteile erfahren, Selektoren als Funktionen zu definieren, anstatt sie inline zu verwenden.
Davor müssen Sie auch Ihren Zustand ändern. Dafür benötigen Sie den dispatch Funktion, die Sie bereits kennengelernt haben. Sie können darauf in Ihrer React-Komponente zugreifen, indem Sie die useAppDispatch Funktion verwenden.
'use client';
import { increment, selectCurrentCount } from './example-reducer';
import { useAppDispatch, useAppSelector } from './hooks';
export default function Home() {
const count = useAppSelector(selectCurrentCount);
const dispatch = useAppDispatch(); // This is store.dispatch.
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h1 className="text-4xl font-bold">Redux Basics</h1>
<div className="flex items-center justify-center space-x-4">
<p className="text-2xl">Count: {count}</p>
<button
className="bg-white text-black hover:bg-white/90 inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium shadow transition-colors"
onClick={() => {
dispatch(increment());
}}
>
Increment
</button>
</div>
</main>
);
}Der useAppDispatch Hook gibt die store.dispatch Methode zurück, die über den Redux-Kontext, der von Ihrem StoreProviderbereitgestellt wird, zugänglich ist. Dann können Sie Ihren Action Creator dispatchen. In diesem Beispiel werden Sie die Increment-Aktion dispatchen, wenn ein Benutzer auf eine Schaltfläche klickt. Das Klicken auf die Schaltfläche aktualisiert den Zustand und ändert dann automatisch den count Wert, den Ihre Komponente empfängt, was ein Re-Rendering auslöst und Ihre Benutzeroberfläche im Browser aktualisiert.
connect HOCDie andere Möglichkeit, Ihren Redux-Store mit Ihrer Komponente zu verbinden, ist über eine Higher-Order Component (HOC). Eine Higher-Order Component ist eine Funktion, die eine Komponente entgegennimmt und eine Komponente zurückgibt. Wenn Sie damit nicht vertraut sind, lesen Sie "Higher-Order Components Are Misunderstood In React".
Die HOC, die React Redux bereitstellt, heißt connect. Refaktorieren Sie Ihre Seitenkomponente, um die Selektoren zu entfernen und connect.
'use client';
import { connect } from 'react-redux';
import { increment, selectCount } from './example-reducer';
// Shadowing: The `increment` function taken in as props is NOT the pure
// action creator imported from './example-reducer'.
// Instead it is a function that automatically dispatches the increment
// action for you.
function Home({ count, increment }) {
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h1 className="text-4xl font-bold">Redux Basics</h1>
<div className="flex items-center justify-center space-x-4">
<p className="text-2xl">Count: {count}</p>
<button
className="bg-white text-black hover:bg-white/90 inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium shadow transition-colors"
onClick={() => {
// No need to wrap increment in dispatch like this:
// dispatch(increment());
increment();
}}
>
Increment
</button>
</div>
</main>
);
}
const mapStateToProps = state => ({ count: selectCount(state) });
const mapDispatchToProps = { increment };
export default connect(mapStateToProps, mapDispatchToProps)(Home);Zuerst passen Sie Ihre Seitenkomponente an, um die count und increment Funktion als Props zu akzeptieren.
Die connect HOC von React Redux verbindet den Redux-Store mit der React-Komponente. Sie nimmt zwei Parameter entgegen:
mapStateToProps ist eine Funktion, die definiert, wie der aktuelle Redux-Store-Zustand in Komponenten-Props umgewandelt wird. In diesem Fall bildet sie den Zustand auf die Props der Home Seitenkomponente ab. Wenn sich der Redux-Zustand, der mit einem der Selektoren zusammenhängt, ändert, löst dies ein erneutes Rendern aus.mapDispatchToProps ist ein Objekt, das definiert, welche Action Creators als Props an die Komponente übergeben werden sollen. Beachten Sie, dass Sie increment NICHT mit dispatch weil mapDispatchToProps erledigt das automatisch für Sie. Die increment Funktion, die Ihre Komponente über ihre Props erhält, unterscheidet sich von dem increment Action Creator, den Sie importieren. Wenn zwei Variablen denselben Namen haben, spricht man von „Shadowing“. In Redux, mit dem connect HOC, kann Shadowing auftreten. Aber lassen Sie sich davon nicht verwirren. Das increment in den Props ist mit dispatch umhüllt, sodass beim Aufruf die „increment“-Aktion an Ihren Store gesendet wird.const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps, // from mapStateToProps
...dispatchProps, // from mapDispatchToProps
...ownProps, // passed in to the component wrapped by connect from its parent
});
connect(mapStateToProps, mapDispatchToProps, mergeProps);Es gibt auch einen dritten Parameter für die connect Higher-Order Component namens mergeProps. Es ermöglicht Ihnen, die Props von Redux mit den Props zu mischen, die von übergeordneten Komponenten übergeben werden. Es wird selten verwendet, daher genügt es, wenn Sie wissen, dass es existiert, und falls Sie jemals darauf stoßen, können Sie es in der Dokumentation nachschlagen.
Zuvor haben Sie zwei Slices eingerichtet: den Beispiel-Slice und den Benutzerprofil-Slice. Der erste Grund, zwei Slices in diesem Artikel zu zeigen, ist, Ihnen combineReducerszu zeigen. Der zweite Grund ist, dass Sie, da der Benutzerprofil-Slice komplexer ist, die Vorteile von Selektoren erkennen können.
Fügen Sie neue Selektoren zu Ihrer src/app/user-profile-reducer.js.
export const loginSucceeded = payload => ({ type: 'LOGIN_SUCCEEDED', payload });
export const usersFetched = payload => ({ type: 'USERS_FETCHED', payload });
export const slice = 'userProfile';
const initialState = { currentUserId: null, users: {} };
export const reducer = (state = initialState, { type, payload } = {}) => {
switch (type) {
case loginSucceeded().type: {
return {
...state,
currentUserId: payload.id,
users: { ...state.users, [payload.id]: payload },
};
}
case usersFetched().type: {
const newUsers = { ...state.users };
payload.forEach(user => {
newUsers[user.id] = user;
});
return { ...state, users: newUsers };
}
default: {
return state;
}
}
};
/**
* Add your selectors below your reducer.
*/
const selectUserProfileSlice = state => state[slice];
export const selectCurrentUsersId = state => state[slice].currentUserId;
export const selectUsers = state => state[slice].users;
export const selectCurrentUser = state =>
state[slice].users[state[slice].currentUserId];
export const selectCurrentUsersEmail = state =>
state[slice].users[state[slice].currentUserId]?.email ?? '';
export const selectCurrentUsersFullName = state =>
`${state[slice].users[state[slice].currentUserId]?.firstName ?? ''} ${state[slice].users[state[slice].currentUserId]?.lastName ?? ''}`;
export const selectIsLoggedIn = state => Boolean(state[slice].currentUserId);selectUserProfileSlice: Ruft den userProfile -Slice des States ab.selectCurrentUsersId: Ermittelt die ID des aktuellen Benutzers aus dem userProfile -Slice.selectUsers: Gibt das users -Objekt aus dem userProfile -Slice zurück.selectCurrentUser: Ruft die vollständigen Daten des aktuellen Benutzers ab. Dabei werden die normalisierten Benutzer genutzt.selectCurrentUsersEmail: Gibt die E-Mail-Adresse des aktuellen Benutzers zurück.selectCurrentUsersFullName: Gibt den aggregierten vollständigen Namen des aktuellen Benutzers zurück.selectIsLoggedIn: Prüft, ob ein Benutzer angemeldet ist.Nachdem Sie nun einige Selektoren kennengelernt haben, können Sie deren Vorteile verstehen.
Eine Fassade ist ein Entwurfsmuster, bei dem Sie eine vereinfachte Schnittstelle zu einem komplexen Subsystem bereitstellen.
Sie haben Abhängigkeiten von der Zustandsstruktur, wenn Ihr Code auf eine bestimmte Form des Zustands angewiesen ist. Eine Änderung der Zustandsstruktur kann den abhängigen Code beschädigen. Die Form Ihres Zustands wird durch seinen Datentyp und seinen Inhalt definiert. Bei Objekten bezieht sich die Form auf deren Verschachtelungsstruktur.
Aktuell sieht Ihr Zustand so aus.
{
"userProfile": {
"currentUsersId": "abc-123",
"users": {
"abc-123": {
"id": "abc-123",
"email": "johndoe@example.com",
"firstName": "John",
"lastName": "Doe"
},
"xyz-789": {
"id": "xyz-789",
"email": "janesmith@example.com",
"firstName": "Jane",
"lastName": "Smith"
}
}
},
"counter": {
"count": 0
}
}Ihre Benutzer sind normalisiert, und jeder Benutzer wird in einem Objekt gespeichert, dessen Schlüssel seine ID ist und dessen Wert der Benutzer selbst ist.
Stellen Sie sich nun vor, dass Sie aus irgendeinem Grund Ihre State-Struktur umgestalten müssten, sodass Ihre Benutzer nicht mehr normalisiert sind, sondern nun ein Array bilden.
{
"userProfile": {
"currentUsersId": "abc-123",
"users": [
{
"id": "abc-123",
"email": "johndoe@example.com",
"firstName": "John",
"lastName": "Doe"
},
{
"id": "xyz-789",
"email": "janesmith@example.com",
"firstName": "Jane",
"lastName": "Smith"
}
]
},
"counter": {
"count": 0
}
}Wenn Sie Ihre Benutzerprofil-Selektoren inline definiert hätten...
function SomeComponent() {
// selectCurrentUsersEmail selector inlined for normalized state
const email = useAppSelector(state =>
state.userProfile.users[state.userProfile.currentUserId]?.email || ''
);
}...müssten Sie diese in jeder Komponente aktualisieren, um das Array zu unterstützen, indem Sie die .find Array-Methode verwenden.
function SomeComponent() {
// Update the selectCurrentUsersEmail selector to work with
// the new array structure.
const email = useAppSelector(state => {
const users = state.userProfile.users;
const currentUserId = state.userProfile.currentUsersId;
const currentUser = users.find(user => user.id === currentUserId);
return currentUser?.email ?? '';
});
// ...
}Sie müssten Ihren Code wie folgt aktualisieren überall wo Sie Benutzer direkt oder einen abgeleiteten Zustand verwenden, wie die E-Mail-Adresse des aktuellen Benutzers.
Dies gilt auch, wenn Sie Selektoren als Funktionen speichern. Sie müssten sie wie folgt umgestalten. (Sie müssen hier nicht mitprogrammieren. Sie werden Ihre State-Struktur nicht wirklich umgestalten. Dies ist immer noch ein hypothetisches Beispiel.)
export const selectCurrentUsersId = state => state[slice].currentUsersId;
export const selectUsers = state => state[slice].users;
export const selectCurrentUser = state =>
state[slice].users.find(user => user.id === state[slice].currentUsersId);
export const selectCurrentUsersEmail = state =>
state[slice].users.find(user => user.id === state[slice].currentUsersId)?.email ?? '';
export const selectCurrentUsersFullName = state => {
const currentUser = state[slice].users.find(
user => user.id === state[slice].currentUsersId
);
return `${currentUser?.firstName ?? ''} ${currentUser?.lastName ?? ''}`;
};
export const selectIsLoggedIn = state => Boolean(state[slice].currentUsersId);Nach dieser Zustandsänderung müssten Sie die drei Selektoren aktualisieren, die auf den aktuellen Benutzer zugreifen:
selectCurrentUser,selectCurrentUsersEmail, undselectCurrentUsersFullName.Alle diese Selektoren müssen nun geändert werden, um find.
Es gab auch eine versteckte Änderung. selectUsers gibt jetzt ein Array anstelle eines Objekts zurück. Dies war jedoch wohl die einzige beabsichtigte Änderung. Alle anderen Selektoren sollten zwar weiterhin dasselbe zurückgeben, mussten aber entsprechend umstrukturiert werden.
Dies zeigt, dass der Code der Selektoren gegen eines der wichtigsten Software-Design-Prinzipien verstößt:
„Eine kleine Anforderungsänderung sollte eine entsprechend kleine Softwareänderung nach sich ziehen.“ — N. D. Birrell, M. A. Ould, "A Practical Handbook for Software Development"
Glücklicherweise verfügen Selektoren über eine weitere wichtige Eigenschaft.
Ihre Selektoren komponieren, was ihre Schlüsselfunktion ist, die es ihnen ermöglicht, Abhängigkeiten von der Zustandsstruktur zu abstrahieren.
Angenommen, Sie kehren zu Ihrer alten Zustandsstruktur zurück, in der Sie Ihre Benutzer normalisiert haben. Dann könnten Sie Ihre Selektoren mithilfe von Funktionskomposition wie folgt schreiben. (Sie sollten diesen Refactor mitprogrammieren.)
// ... existing code
const selectUserProfileSlice = state => state[slice];
const selectCurrentUsersId = state =>
selectUserProfileSlice(state).currentUserId;
const selectUsers = state => selectUserProfileSlice(state).users;
export const selectCurrentUser = state => {
const currentUserId = selectCurrentUsersId(state);
const users = selectUsers(state);
return users[currentUserId];
};
export const selectCurrentUsersEmail = state =>
selectCurrentUser(state)?.email ?? '';
export const selectCurrentUsersFullName = state => {
const currentUser = selectCurrentUser(state);
return `${currentUser?.firstName ?? ''} ${currentUser?.lastName ?? ''}`;
};
export const selectIsLoggedIn = state => Boolean(selectCurrentUsersId(state));selectCurrentUsersId und selectUsers können beide selectUserProfileSlice.
selectCurrentUser kann selectUsers und selectCurrentUsersId um den aktuellen Benutzer einfach zurückzugeben.
Nun, selectCurrentUsersEmail und selectCurrentUsersFullName können beide selectCurrentUser unter der Haube verwenden.
Schließlich kann Ihr selectIsLoggedIn Selektor auch selectCurrentUsersIdnutzen.
Wie Sie sehen, sind auf diese Weise alle Ihre Selektoren sauber abstrahiert. Sie fügen nur das hinzu, was den Zugriff auf ihren jeweiligen Teil des Zustands einzigartig macht – das ist Spezialisierung. Gleichzeitig verbergen sie das Offensichtliche am Zugriff auf ihren Zustand mithilfe anderer Selektoren – das ist Generalisierung.
Stellen Sie sich noch einmal vor, Sie müssten Ihre Benutzer von einer normalisierten Form in ein Array umstrukturieren. (Diese Umstrukturierung bitte wieder überspringen.)
const selectUserProfileSlice = state => state[slice];
const selectCurrentUsersId = state =>
selectUserProfileSlice(state).currentUserId;
const selectUsers = state => selectUserProfileSlice(state).users;
export const selectCurrentUser = state => {
const currentUserId = selectCurrentUsersId(state);
const users = selectUsers(state);
return users.find(user => user.id === currentUserId);
};
export const selectCurrentUsersEmail = state =>
selectCurrentUser(state)?.email ?? '';
export const selectCurrentUsersFullName = state => {
const currentUser = selectCurrentUser(state);
return `${currentUser?.firstName ?? ''} ${currentUser?.lastName ?? ''}`;
};
export const selectIsLoggedIn = state => Boolean(selectCurrentUsersId(state));Sie müssten nur eine Zeile im selectCurrentUser Selektor ändern. Alle anderen Selektoren könnten gleich bleiben.
Und das ist es, was mit dem Punkt gemeint ist, dass Selektoren Abhängigkeiten von der Zustandsstruktur abstrahieren, weil sie komponierbar sind. Wenn sich Ihre Zustandsstruktur ändert, müssen Sie normalerweise nur die Selektoren anpassen, die direkt Ihrer Strukturänderung entsprechen. Im Gegensatz dazu erfordern nicht-komponierte Selektoren Änderungen an vielen Stellen, was die Wahrscheinlichkeit von Fehlern erhöht.
Wenn Sie Memoizing verstehen, sollte klar sein, dass Selektoren als reine Funktionen memoisiert werden können. Wenn Sie mit Memoizing nicht vertraut sind, lesen Sie diesen Artikel weiter, nachdem Sie „Was ist Memoization? (In JavaScript & TypeScript)“ gelesen haben, der Memoization ausführlich erklärt.
Um zu erfahren, wie Sie Ihre Selektoren memoizieren können, lesen Sie den dritten Artikel dieser Redux-Reihe. Er behandelt die createSelector API aus dem Redux Toolkit, die Sie verwenden können, um Ihre Selektoren zu memoizieren.
Ziel dieses Artikels ist es, Ihnen ein Verständnis von Redux auf Senior-Niveau zu vermitteln. Heben wir also die Codequalität weiter an.
Sie können Ihre Selektoren mithilfe funktionaler Programmierung noch weiter aufräumen.
Hinweis: Wenn Sie neu in der funktionalen Programmierung sind, lesen Sie entweder „Entfesseln Sie das Potenzial von JavaScript mit funktionaler Programmierung“ bevor Sie fortfahren, oder überspringen Sie diesen Abschnitt und springen Sie zu „Display- / Container-Komponenten-Muster“ weiter unten.
Beim Lesen dieses Refactorings möchten Sie diesen Artikel vielleicht in einem neuen Tab öffnen und das Refactoring Seite an Seite mit seiner vorherigen Version ohne funktionale Programmierung vergleichen.
// ... actions & reducer
const prop = key => obj => obj[key];
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const converge = (merger, fns) => x => merger(...fns.map(fn => fn(x)));
const propOr = defaultValue => key => obj => obj[key] ?? defaultValue;
const selectUserProfileSlice = prop(slice);
const selectCurrentUsersId = pipe(selectUserProfileSlice, prop('currentUserId'));
const selectUsers = pipe(selectUserProfileSlice, prop('users'));
export const selectCurrentUser = converge(prop, [
selectCurrentUsersId,
selectUsers,
]);
export const selectCurrentUsersEmail = pipe(
selectCurrentUser,
propOr('', 'email'),
);
export const selectCurrentUsersFullName = pipe(
selectCurrentUser,
converge(
(firstName, lastName) => `${firstName} ${lastName}`,
[propOr('', 'firstName'), propOr('', 'lastName')],
),
);
export const selectIsLoggedIn = pipe(selectCurrentUsersId, Boolean);Sie werden vier Funktionen verwenden.
prop: Ruft die angegebene Eigenschaft von einem Objekt ab. Dies ist nützlich, um direkt auf bestimmte Teile des Zustands zuzugreifen.pipe: Kombiniert mehrere Funktionen zu einer einzigen Funktion, die von links nach rechts ausgeführt wird, wobei der Rückgabewert jeder Funktion an die nächste übergeben wird.converge: Akzeptiert mehrere Selektoren (oder Transformationen) und eine Kombinationsfunktion. Die Selektoren sammeln Daten aus dem Zustand, und die Kombinationsfunktion führt diese Daten zusammen.propOr: Ähnlich wie prop, bietet aber einen Standardwert, falls die angegebene Eigenschaft im Objekt fehlt.Alle diese Funktionen sind curried und werden mit partiellen Anwendungen verwendet.
Verwenden Sie diese Hilfsfunktionen, um jeden der Selektoren zu refaktorisieren.
Ihre Selektoren verhalten sich immer noch genau gleich, sind aber viel sauberer und deklarativer. Mit der Zeit wird das Lesen und Schreiben von deklarativem Code intuitiver werden als die alte, imperative Methode.
Alle Hilfsfunktionen sind in Ramda verfügbar, also installieren Sie es.
npm i ramdaLöschen Sie Ihre benutzerdefinierten Hilfsfunktionen und importieren Sie stattdessen die Funktionen aus Ramda.
import { converge, pipe, prop, propOr } from 'ramda';
// ... rest of your reducers & selectorsSie können die Selektoren des counter Slices ebenfalls refaktorisieren.
// ... existing code
const selectExampleSlice = prop(slice);
const selectCount = pipe(selectExampleSlice, prop('count'));Hier verwenden Sie auch prop , um auf die verschiedenen Slices zuzugreifen, und pipe , um Ihre Selektoren zu komponieren.
Der connect HOC funktioniert gut mit dem Display-/Container-Komponentenmuster. Falls Sie damit nicht vertraut sind, organisiert dieses Muster Komponenten in zwei Kategorien:
connect HOC sehen werden.Erstellen Sie eine Display-Komponente unter src/app/user-profile-component.js.
export const UserProfileComponent = ({ isLoggedIn, email, onLoginClicked }) => (
<div className="flex items-center space-x-4">
{isLoggedIn ? (
<p className="text-2xl">Email: {email}</p>
) : (
<button
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md bg-white px-4 py-2 text-sm font-medium text-black shadow transition-colors hover:bg-white/90"
onClick={() =>
onLoginClicked({ id: '123', email: 'jan@reactsquad.io' })
}
>
Login
</button>
)}
</div>
);Die Komponente nimmt einen isLoggedIn Booleschen Wert entgegen und rendert die E-Mail des angemeldeten Benutzers oder einen Button, der beim Klicken den Benutzer anmeldet.
Mit dieser Display-Komponente können Sie Ihre User-Profile-Action-Creators und Selektoren mithilfe des connect HOC in einer Container-Komponente kombinieren.
'use client';
import { connect } from 'react-redux';
import {
loginSucceeded,
selectCurrentUsersEmail,
selectIsLoggedIn,
} from './user-profile-reducer';
import { UserProfileComponent } from './user-profile-component';
const mapStateToProps = state => ({
email: selectCurrentUsersEmail(state),
isLoggedIn: selectIsLoggedIn(state),
});
const mapDispatchToProps = { onLoginClicked: loginSucceeded };
export default connect(
mapStateToProps,
mapDispatchToProps,
)(UserProfileComponent);Der connect Funktion verbindet den Redux-Store mit dem UserProfileComponent. Sie verwendet mapStateToProps , um Redux-Store-Updates über Selektoren zu abonnieren und den Zustand (State) auf Komponenten-Props abzubilden. Sie verwendet mapDispatchToProps , um Action Creators an den Redux-Store-Dispatch zu binden, wodurch die Komponente Aktionen auslösen kann.
Beachten Sie, wie diese Datei src/app/user-profile-container.js jetzt kein JSX mehr enthält.
Importieren Sie nun die UserProfileContainer und rendern Sie sie in Ihrer Home Komponente.
'use client';
import { increment, selectCount } from './example-reducer';
import { useAppDispatch, useAppSelector } from './hooks';
import UserProfileContainer from './user-profile-container';
export default function Home() {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
return (
<main className="flex min-h-screen flex-col items-center p-24">
<h1 className="text-4xl font-bold">Redux Basics</h1>
<div className="flex items-center justify-center space-x-4">
<p className="text-2xl">Count: {count}</p>
<button
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md bg-white px-4 py-2 text-sm font-medium text-black shadow transition-colors hover:bg-white/90"
onClick={() => {
dispatch(increment());
}}
>
Increment
</button>
</div>
<UserProfileContainer />
</main>
);
}Wenn Sie Ihre App jetzt ausführen, können Sie sich „anmelden“ und die E-Mail in der Benutzeroberfläche sehen.
Warnung: Der Schwierigkeitsgrad wird hier ansteigen, aber entspannen Sie sich, denn dieser Artikel wird jeden Schritt für Sie aufschlüsseln.
Nun lernen Sie den letzten Baustein kennen: Middleware.
Sie werden zwei Anwendungsfälle sehen, die üblicherweise über Middleware abgewickelt werden: Logging und Datenabruf.
Stellen Sie sich vor, Sie möchten Ihre Redux-App debuggen und die Aktionen protokollieren, die Sie dispatchen, sowie den Zustand, nachdem die Aktion verarbeitet wurde.
export function dispatchAndLog(store, action) {
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
}
dispatchAndLog(store, incrementBy(42));Sie könnten eine dispatchAndLog Funktion erstellen, die die Aktion protokolliert, bevor sie dispatched wird, und dann den resultierenden Zustand protokolliert.
import { incrementBy } from './example-reducer';
import { useAppStore } from './hooks';
import { dispatchAndLog } from './temp-wrappers';
function SomeComponent() {
const store = useAppStore();
return (
<button
onClick={() => {
dispatchAndLog(store, incrementBy(42));
}}
>
Click Me
</button>
);
}Sie können sie in einer Komponente importieren und aufrufen, indem Sie Ihren Store und eine Aktion übergeben.
Wenn Sie Ihre App ausführen und auf die Schaltfläche klicken, werden die Aktion und der Zustand protokolliert, nachdem Ihre Aktion verarbeitet wurde.
dispatching { type: 'INCREMENT_BY', payload: 42 }
next state { example: { count: 42 }, userProfile: { currentUserId: null, users: {} } }Nun zum zweiten Anwendungsfall: dem Abrufen von Daten. Eine der gängigsten Methoden, den Datenabruf in Redux zu handhaben, sind „Thunks“.
Ein Thunk ist im Wesentlichen eine Unterroutine, die verwendet wird, um eine zusätzliche Berechnung in eine andere Unterroutine einzuschleusen. Sie verzögert die Berechnung eines Wertes, bis dieser benötigt wird, und kann auch Operationen zum Zeitpunkt der Ausführung einfügen.
// calculation of x is immediate
const truth = 21 + 21; // 42
// thunk: calculation of x is delayed
const getTruth = () => 21 + 21;Der Begriff „Thunk“ wurde geprägt, nachdem seine Erfinder während einer nächtlichen Diskussion erkannten, dass einige rechnerische Aspekte vorab berechnet werden könnten, oder „bereits gedacht worden waren“. Sie nannten es humorvoll „Thunk“ und scherzten, es sei die Vergangenheitsform von „think“ um zwei Uhr morgens.
Sie haben gelernt, dass Aktionen Objekte sind. Doch mit Middleware können Aktionen mehr sein. Mithilfe von Thunk-Middleware können Aktionen auch Funktionen sein. Anstatt ein Aktionsobjekt direkt zu dispatchen, dispatchen Sie eine Funktion – den Thunk –, die asynchrone Aufgaben ausführen und dann basierend auf dem Ergebnis weitere Aktionen dispatchen kann.
function fetchDataThunk() {
// `getState` is never used by this particular thunk, but all
// thunks have access to both `dispatch` and `getState`.
return async (dispatch, getState) => {
dispatch({ type: 'data/fetchStart' });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'data/fetchSuccess', payload: data });
} catch (error) {
dispatch({ type: 'data/fetchFailure', error });
}
};
}Wenn eine Aktion eine Funktion ist, nennt man sie einen „Thunk“. In Redux ist ein Thunk eine Higher-Order-Funktion, die eine andere Funktion zurückgibt. Diese zurückgegebene Funktion akzeptiert die dispatch Funktion als ersten Parameter und die getState Funktion als zweiten, und gibt ein Promise zurück, das mit nichts aufgelöst wird. Sie erhält Zugriff auf diese Argumente von einem Wrapper, den Sie gleich sehen werden.
Man kann ein fetchDataThunk erstellen, um asynchrone API-Aufrufe zu verwalten. Es löst verschiedene Aktionen aus, je nachdem, ob der Abruf gestartet, erfolgreich war oder fehlgeschlagen ist.
So könnte man einen manuellen dispatchAsync Wrapper schreiben, um Thunks zu verwalten.
export function dispatchAsync(store, actionOrThunk) {
if (typeof actionOrThunk === 'function') {
// If it's a function, call it with dispatch and getState.
return actionOrThunk(store.dispatch, store.getState);
} else {
// If it's an action object, dispatch it directly.
return store.dispatch(actionOrThunk);
}
}Diese dispatchAsync Funktion prüft, ob das übergebene Argument eine Funktion ist. Wenn ja, geht sie davon aus, dass es sich um einen Thunk handelt, und ruft ihn mit den dispatch und getState Methoden aus dem Store auf. Andernfalls löst sie das Action-Objekt wie gewohnt aus.
Man kann dispatchAsync in der Komponente verwenden, um diesen Thunk zu verwalten.
import { useAppStore } from './hooks';
import { fetchData } from './temp-thunk-example';
import { dispatchAsync } from './temp-wrappers';
function SomeComponent() {
const store = useAppStore();
return (
<button
onClick={() => {
dispatchAsync(store, fetchData());
}}
>
Fetch Data
</button>
);
}Man umschließt das Auslösen des fetchData() Thunks einfach mit dispatchAsync.
Wenn nun der „Daten abrufen“-Button geklickt wird, dispatchAsync prüft, ob fetchData() eine Funktion ist (was sie auch ist) und ruft sie dann mit den dispatch und getState -Methoden des Stores auf. Der Thunk kann dann die asynchrone Abrufoperation durchführen und die entsprechenden Aktionen basierend auf dem Ergebnis dispatchen.
Um zu beweisen, dass dies funktioniert, können Sie beide Ihrer Wrapper kombinieren.
import { useAppStore } from './hooks';
import { fetchDataThunk } from './temp-thunk-example';
import { dispatchAndLog, dispatchAsync } from './temp-wrappers';
function SomeComponent() {
const store = useAppStore();
return (
<button
onClick={() => {
// Compose the two wrappers so that actions dispatched by the thunk get
// logged out.
dispatchAsync(store, (dispatch, getState) => {
// Wrap the dispatch function with dispatchAndLog.
const dispatchWithLog = action => dispatchAndLog(store, action);
// Call the thunk with the wrapped dispatch.
return fetchDataThunk()(dispatchWithLog, getState);
});
}}
>
Fetch Data
</button>
);
}Im onClick -Handler rufen Sie dispatchAsyncauf. Anstatt den Thunk direkt zu übergeben, übergeben wir eine Funktion, die dispatch und getStateentgegennimmt. Sie erstellen ein dispatchWithLog Funktion, die den Dispatch des Stores mit dispatchAndLog. Anschließend geben Sie die fetchDataThunk, die sofort aufgerufen wird und ihre asynchrone innere Funktion zurückgibt. Diese innere Funktion wird dann mit dispatchWithLog und getStateaufgerufen. Dadurch läuft jede innerhalb des Thunks ausgelöste Aktion über dispatchAndLog, die die Aktion und den nächsten Zustand protokolliert.
Das wird nun protokolliert, wenn Sie Ihre App ausführen und auf „Fetch Data“ klicken und der Abruf erfolgreich ist.
dispatching { type: 'data/fetchStart' }
next state { example: { count: 0 }, userProfile: { currentUserId: null, users: {} } }
dispatching { type: 'data/fetchSuccess', payload: { some: 'data' } }
next state { example: { count: 0 }, userProfile: { currentUserId: null, users: {} } }Schlägt es fehl, würde Folgendes protokolliert werden.
dispatching { type: 'data/fetchStart' }
next state { example: { count: 0 }, userProfile: { currentUserId: null, users: {} } }
dispatching { type: 'data/fetchFailure', error: { message: 'Error message' } }
next state { example: { count: 0 }, userProfile: { currentUserId: null, users: {} } }Man kann sich vorstellen, dass das Importieren dieser Wrapper-Funktionen in jeder Komponente und das Zusammensetzen aller Wrapper, wo Sie dispatch umständlich ist. Und Sie bräuchten für jede neue Anforderung eine neue Wrapper-Funktion.
Middleware bietet eine einfachere Lösung. Middleware ermöglicht es Ihnen, Aktionen abzufangen, nachdem Sie sie versendet haben, aber bevor sie Ihre Reducer erreichen.
const someMiddleware = store => next => action => {
// ... the middleware logic, which returns `next(action)`.
}Middleware in Redux sind Currying-Funktionen, die zuerst ein Store Objekt, dann eine Dispatch Funktion (genannt next), und schließlich die aktuelle Aktion. Middleware gibt eine Funktion zurück, die eine Aktion entgegennimmt und optional einen Wert zurückgibt. Dieser Wert ist normalerweise die nächste Middleware.
Eine Logger-Middleware, die den zuvor genannten Anwendungsfall löst, sieht so aus.
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};Es gibt aber noch viele weitere Middleware.
Um die Logger-Middleware und andere zu verstehen, müssen Sie zuerst etwas über Redux' applyMiddleware Funktion lernen. applyMiddleware ist unerlässlich, um Middleware auszuführen. Wenn Sie die Middleware Ihrem Store zur Verfügung stellen, muss es eine applyMiddleware Funktion geben, die all Ihre Middleware umschließt. Der beste Weg, um applyMiddleware geschieht, indem Sie Ihre eigene Version von Grund auf neu programmieren.
Erstellen Sie eine Funktion namens applyMiddleware, aktualisieren Sie dann Ihre benutzerdefinierte createStore Funktion, um mit Middleware zu arbeiten.
(Hinweis: Das folgende Codebeispiel ist zu Demonstrationszwecken absolut eigenständig, sodass Sie sich auf die neue applyMiddleware und die modifizierte createStore Funktion konzentrieren können. Mit anderen Worten, Sie müssen nicht mitprogrammieren. Aber Sie könnten diesen Artikel jedoch in einem neuen Tab erneut öffnen wollen, um den Code und die Erklärung unten nebeneinander lesen zu können.)
function applyMiddleware(...middlewares) {
return function enhancer(createStore, reducer, initialState) {
const store = createStore(reducer, initialState);
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...arguments_) => enhancedDispatch(action, ...arguments_),
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
const enhancedDispatch = chain.reduceRight(
(currentDispatch, currentMiddleware) => currentMiddleware(currentDispatch),
store.dispatch,
);
return { ...store, dispatch: enhancedDispatch };
};
}
export function createStore(reducer, initialState, enhancer) {
if (enhancer) {
return enhancer(createStore, reducer, initialState);
}
let state = initialState;
const dispatch = action => {
state = reducer(state, action);
};
const getState = () => state;
return { dispatch, getState };
}
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
const thunk = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
const rootReducer = (state = { count: 0 }, { payload, type } = {}) => {
switch (type) {
case 'INCREMENT': {
return { ...state, count: state.count + 1 };
}
case 'DECREMENT': {
return { ...state, count: state.count - 1 };
}
default: {
return state;
}
}
};
const store = createStore(
rootReducer,
rootReducer(),
applyMiddleware(logger, thunk),
);
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
const incrementThunk = (dispatch, getState) => {
console.log('Current state before async:', getState());
setTimeout(() => {
dispatch({ type: 'INCREMENT' });
console.log('State after async:', getState());
}, 5000);
}
store.dispatch(incrementThunk);applyMiddleware ist curried und nimmt eine beliebige Anzahl von Middleware mithilfe des Spread-Operators entgegen, sodass Sie im Funktionskörper Zugriff auf ein Array von Middleware haben.
Sie gibt eine Funktion namens enhancer zurück, die Ihre createStore Funktion, ein reducer und ein initialState. Diese Argumente werden an den enhancer aus createStore, den Sie gleich sehen werden.
Es erstellt einen neuen Store unter Verwendung Ihrer createStore Funktion.
Anschließend erstellt es ein middlewareAPI Objekt, das eine getState Methode und eine dispatch Methode, genau wie der Store Objekt.
Anschließend iteriert es über alle Middleware und ruft sie mit der middlewareAPIauf. Nun haben Sie ein Array von partiell angewendeter Middleware, genannt chain. Sie alle haben Zugriff auf die middlewareAPI, da sie als das Store -Argument bereitgestellt wurde. Angenommen, Sie rufen chain mit der logger und thunk -Middleware auf, dann sieht chain so aus.
const chain = [
// This IIFE is just there to visualize the partial application for you.
(store => (next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
}))({ /* middlewareAPI */}),
// This is how it really looks like 👇 `store` is the `middlewareAPI`.
next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
},
];Beachten Sie, dass Store hier das applyMiddleware -Objekt ist.
Die innere Funktion von applyMiddleware verwendet dann reduceRight über diese partiellen Anwendungen in chain um die neueste Version von dispatch genannt enhancedDispatch.
Man beachte, Middleware empfängt im Allgemeinen einen Store, den vorherigen Dispatch, genannt next und gibt eine neue Dispatch-Funktion zurück, die eine Aktion als Argument entgegennimmt.
// const middleware = store => previousDispatch => function newDispatch(action) {
const middleware = store => next => action => {
// ...
}Bei der Verwendung mehrerer Middleware-Funktionen empfängt jede einen next Parameter, der die Dispatch-Funktion ist, die von der vorherigen Middleware in der Kette modifiziert wurde. Dies ermöglicht es jeder Middleware, das Dispatch-Verhalten der vorhergehenden Middleware im chain Array, oder den ursprünglichen Store-Dispatch, wenn es die erste Middleware ist.
Sie verwenden reduceRight weil Middleware normalerweise mit der Erwartung geschrieben wird, dass sie die Aktion erhält, bevor eine zuvor registrierte Middleware sie verarbeitet. Das bedeutet, dass Middleware in umgekehrter Reihenfolge angewendet werden sollte, wie Aktionen sie durchlaufen.
Gehen wir das Schritt für Schritt durch.
1.) Ursprünglicher dispatch - Es beginnt mit der ursprünglichen dispatch Funktion aus dem Store.
const dispatch = store.dispatch;2.) Anwenden von thunk - Die thunk Middleware ist die erste, die das ursprüngliche Dispatch umhüllt, indem sie reduceRight mit dem store in ihrem Closure erfasst. Sie prüft, ob die Aktion eine Funktion ist, und führt sie gegebenenfalls aus; andernfalls wird fortgefahren.
const dispatch = store.dispatch;
const middleware = next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};3.) Anwenden von logger - Die Logger Middleware umschließt dann den durch die Thunk Middleware modifizierten Dispatch. Sie protokolliert die Aktion und den Zustand vor und nach der Verarbeitung der Aktion.
const dispatch = action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return store.dispatch(action);
};
const middleware = next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};4.) Abschließender enhancedDispatch – Der enhancedDispatch ist nun vollständig zusammengesetzt und integriert beide Middleware. Zuerst protokolliert er die Details der Aktion, verarbeitet dann Thunks und protokolliert schließlich den aktualisierten Zustand. Der Store bleibt über die Closure aus der Erstellung der chainzugänglich.
const enhancedDispatch = action => {
console.log('dispatching', action);
let result = (action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return store.dispatch(action);
})(action);
console.log('next state', store.getState());
return result;
};applyMiddleware gibt schließlich ein neues Store-Objekt mit dem modifizierten Dispatch zurück.
Ihre createStore -Funktion sollte nun einen dritten Parameter namens enhancerakzeptieren. Wenn der Enhancer bereitgestellt wird, gibt sie den Enhancer zurück und ruft ihn zuerst mit der createStore Funktion selbst auf, dann mit dem reducer und initialState. Dies gibt eine erweiterte Version des Stores zurück, bei der die dispatch Funktion durch die enhancedDispatch ersetzt wurde, die die Middleware-Logik enthält. Der Rest von createStore bleibt unverändert.
Springen wir nun vorwärts und sehen uns an, wie Sie Ihre aktualisierte createStore Funktion verwenden. Sie übergeben applyMiddleware als drittes Argument, und applyMiddleware wird mit Ihrer Middleware aufgerufen. Da applyMiddleware eine Funktion mit der gleichen Signatur wie createStore – die einen reducer und initialState, und den Store zurückgibt – bleibt das ursprüngliche Verhalten erhalten, wenn Middleware angewendet wird. Wenn Sie die createStore Funktion innerhalb von applyMiddleware ohne einen enhancer, funktioniert sie normal.
Middleware vereinfacht die Lösung des Logging-Problems. Die logger Middleware protokolliert die aktuelle Aktion und ruft die nächste Middleware in der Kette auf. Nachdem alle Middleware die Aktion verarbeitet hat, protokolliert sie den Zustand des Stores und gibt das Ergebnis zurück.
Die thunk Middleware erweitert die Fähigkeiten der Redux-Dispatch-Funktion, sodass Sie asynchrone Operationen oder komplexe synchrone Logik handhaben können. Wenn die ausgelöste Aktion eine Funktion anstelle eines regulären Objekts ist, dann fängt die Thunk -Middleware sie ab und übergibt dispatch und getState als Argumente an diese Funktion.
Definieren Sie nun einen einfachen rootReducer , der eine INCREMENT - und eine DECREMENT -Aktion verarbeiten kann, und richten Sie dann den Store mit Ihrer Middleware ein.
Lösen Sie dann einige Aktionen als Objekte und als Funktionen aus. Die Funktion kann auch eine asynchrone Funktion sein.
Wenn Sie dieses Beispiel ausführen, erhalten Sie die folgende Ausgabe.
$ npx tsx src/app/temp-apply-middleware-example.js
dispatching { type: 'INCREMENT' }
next state { count: 1 }
dispatching { type: 'DECREMENT' }
next state { count: 0 }
Current state before async: { count: 0 }
dispatching { type: 'INCREMENT' }
next state { count: 1 }
State after async: { count: 1 }Die Logger-Funktion protokolliert Ihre Aktionen und den Zustand nach jeder Aktion. Und Sie können sehen, dass es eine Verzögerung gibt, bevor der Thunk seine Logik ausführt. Der Logger erfasst die vom Thunk verarbeiteten Aktionen, da beide Middleware jede Aktion verarbeiten.
Die Logger- und Thunk-Middleware sind beide als Pakete verfügbar, also installieren Sie sie.
npm i redux-logger redux-thunkRedux exportiert auch applyMiddleware. Importieren Sie es zusammen mit der Middleware.
import {
applyMiddleware,
combineReducers,
legacy_createStore as createStore,
} from 'redux';
import logger from 'redux-logger';
import { thunk } from 'redux-thunk';
import { reducer as exampleReducer, slice as exampleSlice } from './example-reducer';
import {
reducer as userProfileReducer,
slice as userProfileSlice,
} from './user-profile-reducer';
const rootReducer = combineReducers({
[exampleSlice]: exampleReducer,
[userProfileSlice]: userProfileReducer,
});
export const makeStore = () => {
return createStore(
rootReducer,
rootReducer(),
applyMiddleware(logger, thunk)
);
};Schließlich können Sie Ihre makeStore Funktion anpassen, um die Middleware einzurichten.
Wenn Sie nun Aktionen in Ihrer App auslösen, werden diese protokolliert. Und Sie könnten jetzt auch Thunks auslösen.
Beantworten wir nun die Fragen, die zu Beginn dieses Artikels aufgeworfen wurden.
Redux wurde aufgrund der folgenden Eigenschaften populär:
Um diese Probleme anzugehen, Server-State-Bibliotheken wie React Query oder useSWR populär. Moderne Anwendungen, die mit den neuesten Versionen von Next.js oder Remix erstellt wurden, machen ein globales Zustandsmanagement überflüssig. Diese Bibliotheken und Frameworks verwalten den Zustand im Hintergrund und stellen nur das zur Verfügung, was Sie zum Erstellen dynamischer Webanwendungen benötigen, wodurch Redux oft überflüssig wird.
Doch wie zu Beginn dieses Artikels erwähnt, wird Redux immer noch intensiv in vielen Anwendungen eingesetzt, weshalb es für Sie von enormem Wert ist, es zu kennen.
Dieser Artikel war sehr theorielastig, und in dieser Serie werden Sie zwei reale Anwendungen mit Redux programmieren, damit Sie praktische Erfahrungen sammeln können.
ReactSquad ist die beste Plattform für die Einstellung spezialisierter Redux-Entwickler zu erschwinglichen Preisen. Wenn Sie interessiert sind, Gespräch vereinbaren noch heute mit uns.