Redux

Meistern Sie die Best Practices für Redux-Aktionen & machen Sie das Debugging einfach

Ich habe kürzlich die ersten Artikel und Videos auf meinem YouTube-Kanal als Teil einer Serie über Redux veröffentlicht.

Als ich mit meinen Mentees sprach, hatten einige von ihnen Schwierigkeiten, ihre Actions richtig umzusetzen. Fairerweise muss man sagen, dass, obwohl Actions einfach erscheinen, kleine Fehler sich summieren und zu erheblichen technischen Schulden führen können.

Deshalb wollte ich diese Bonus-Mikrolektion für euch schreiben, um die Best Practices für Actions detailliert darzulegen. Alles, was du gleich lernen wirst, gilt für jede Action-Design – egal ob du Vanilla Redux, Redux Toolkit oder Reacts integrierten useReducer Hook verwendest.

Bevor wir dazu kommen, falls du Teil eins noch nicht gelesen hast, „Was ist Redux? (Ein fortgeschrittenes Verständnis der Funktionsweise von Redux erlangen)“ und Teil zwei, „Redux Saga ist schwer, bis man unter die Haube schaut“, öffne sie in einem neuen Tab und lies sie zuerst. Komm dann zurück. Dieser Artikel setzt voraus, dass du die sechs Bausteine von Redux und die Grundlagen von Redux Saga kennst.

Zur kurzen Wiederholung: Redux-Actions sind Objekte mit einem Typ und eine optionale Payload.

const loginClicked = {
  type: 'LOGIN_CLICKED',
  payload: { email: 'jan@reactsquad.io', password: '5ub5cr1b3' },
};

Diese Aktionen stammen normalerweise von Action Creators, das sind Funktionen, die Action-Objekte zurückgeben.

const loginClicked = (email, password) => ({
  type: 'LOGIN_CLICKED',
  payload: { email, password },
});

Aussagekräftige Namen

Die erste bewährte Methode ist, dass Ihre Aktionen aussagekräftige Namenhaben sollten. Diese Namen sollten beschreiben, was in Ihrer App passiert ist, anstatt was die Aktion tut. Das hilft beim Debugging. Die meisten Leute benennen ihre Aktionen nach dem, was die Aktion tut, was es schwieriger macht, ihre Rolle im Kontext Ihrer App zu verstehen.

Lassen Sie mich Ihnen ein reales Beispiel geben, das bei einem meiner Mentees aufkam. Er wollte das Gelernte aus den Redux-Videos üben und baute ein Tipptrainer-Spiel. In diesem Spiel tippt der Benutzer bestimmte Zeichenketten, die viele Sonderzeichen enthalten. Bei jedem Tastendruck prüft das Spiel, ob der Benutzer die richtigen Zeichen eingegeben hat, und wenn der Benutzer alles richtig gemacht hat, speichert es das Level.

Am Anfang entwarf er seinen Zustand mit einer einzigen setInputString Aktion.

export const setInputString = payload => ({
  type: 'SET_INPUT_STRING',
  payload,
});

export const sliceName = 'typeTrainer';

const initialState = {
  inputString: '',
  levelString: '{}/^%*#!)@(',
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case setInputString().type: {
      return { ...state, inputString: payload };
    }
    default: {
      return state;
    }
  }
};

export const selectTypeTrainerSlice = state => state[sliceName];

export const selectInputString = state =>
  selectTypeTrainerSlice(state).inputString;

export const selectLevelString = state =>
  selectTypeTrainerSlice(state).levelString;

Er erstellte eine setInputString Aktion, die immer dann ausgelöst wurde, wenn der Benutzer etwas in das Eingabefeld eingab.

Der Zustand enthielt die aktuelle Eingabezeichenkette und die Level-Zeichenkette. Die Level-Zeichenkette stellte den Text dar, den der Benutzer für jedes Level korrekt eingeben musste.

Im Reducer aktualisierte er den Zustand mit der neuen Eingabezeichenkette.

Des Weiteren hatte er Selektoren, um sowohl die aktuelle Eingabezeichenkette als auch die Level-Zeichenkette zu lesen.

Dieser Zustand ist stark vereinfacht, da ich nur die Bedeutung aussagekräftiger Aktionsnamen veranschaulichen möchte.

Er hatte auch eine Saga, die auf alle Änderungen der Eingabezeichenkette lauschte und prüfte, ob der Benutzer die richtigen Zeichen eingegeben hatte.

import { put, takeLeading } from 'redux-saga/effects';

import {
  selectInputString,
  selectLevelString,
  setInputString,
} from './type-trainer-reducer';

export function* handleInputString() {
  const inputString = yield select(selectInputString);
  const levelString = yield select(selectLevelString);

  if (inputString === levelString) {
    // Perform side effects like showing confetti or recording CPM.
    // Example: yield put(showConfetti);
    // Example: yield call(recordCharactersPerMinute);

    // Reset the input string.
    yield put(setInputString(''));
  }
}

export function* watchInputString() {
  yield takeLeading(setInputString.type, handleSetInputString);
}

Die Saga würde auf alle Änderungen in der Eingabezeichenkette lauschen und prüfen, ob der Benutzer die richtigen Zeichen eingegeben hat. Wenn der Benutzer die richtigen Zeichen eingegeben hat, verwendete er die setInputString Aktion in der Saga, um die Eingabezeichenkette zurückzusetzen. Tatsächlich wurde die Aktion sogar ein drittes Mal verwendet, um die Eingabe zurückzusetzen, wenn der Benutzer einen Fehler gemacht hat.

Dieses Beispiel veranschaulicht nun, das Problem mit nicht aussagekräftigen Aktionsnamen:

Wenn es einen Fehler im Code gibt, ist es schwer nachzuvollziehen, woher er kam. Infolgedessen lassen sich die Sagas auch schwer in den Kontext einordnen. Im Moment sieht es so aus, als sollten diese Sagas immer dann ausgeführt werden, wenn sich die Eingabezeichenkette ändert, selbst wenn sie durch sich selbst ausgelöst werden – wie zum Beispiel bei einem Reset.

Um dieses Problem zu beheben, solltest du der Aktion einen aussagekräftigen Namen geben, der erklärt, was gerade passiert ist, anstatt zu beschreiben, was die Aktion tut.

export const userTyped = payload => ({ type: 'USER_TYPED', payload });
export const userSucceeded = () => ({ type: 'USER_SUCCEEDED' });

export const sliceName = 'typeTrainer';

const initialState = {
  inputString: '',
  levelString: '{}/^%*#!)@(',
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case userTyped().type: {
      return { ...state, inputString: payload };
    }
    case userSucceeded().type: {
      return { ...state, levelString: '' };
    }
    default: {
      return state;
    }
  }
};

// ... selectors

Nun beschreibt die Aktion userTyped was passiert ist. Zusätzlich fügst du eine zweite Aktion hinzu, userSucceeded, um zu beschreiben, was passiert ist, als der Benutzer die richtigen Zeichen eingegeben hat.

Du kannst diese Aktionen nun in deiner Saga verwenden.

import { put, takeLeading } from 'redux-saga/effects';

import {
  selectInputString,
  selectLevelString,
  userSucceeded,
  userTyped,
} from './type-trainer-reducer';

export function* handleUserTyped() {
  const inputString = yield select(selectInputString);
  const levelString = yield select(selectLevelString);

  if (inputString === levelString) {
    // Perform side effects like showing confetti or recording CPM.
    // Example: yield put(showConfetti);
    // Example: yield call(recordCharactersPerMinute);

    // Reset the input string.
    yield put(userSucceeded());
  }
}

export function* watchUserTyped() {
  yield takeLeading(userTyped.type, handleUserTyped);
}

Nun siehst du, dass die Saga nur auf die userTyped Aktion. Wenn der Benutzer die richtigen Zeichen eingibt, wird die userSucceeded Aktion stattdessen ausgelöst. Dies kommuniziert klar, was die Saga innerhalb der App-Funktionalität tut, anstatt nur zu beschreiben, was die Aktion programmatisch tut.

Benannte Parameter

Einige Action-Creator akzeptieren mehrere Werte in ihrem Payload.

const loginClicked = (email, password) => ({
  type: 'LOGIN_CLICKED',
  payload: { email, password },
});

Sie können Action-Creator leichter lesbar machen, indem Sie benannte Parameterverwenden.

Falls Sie noch nie von benannten Parametern gehört haben, ermöglichen sie es Ihnen, Funktionsargumente zu spezifizieren, indem Sie jeden Parameter explizit benennen und ihm beim Aufruf der Funktion einen Wert zuweisen. Dies macht deutlich, welcher Wert welchem Parameter entspricht, und ermöglicht es, Argumente in beliebiger Reihenfolge anzugeben.

Technisch gesehen unterstützt JavaScript keine benannten Parameter, daher hier ein Beispiel in Python, das benannte Parameter unterstützt.

def login(email, password):
    # TODO: Implement the login functionality
    pass

login(email="user@example.com", password="securepassword123")

Wie oben erwähnt, können Sie die Funktion mit den Parametern in beliebiger Reihenfolge aufrufen. Beide dieser Aufrufe sind also äquivalent:

login(email="user@example.com", password="securepassword123")
login(password="securepassword123", email="user@example.com")

In JavaScript können Sie benannte Parameter emulieren, indem Sie ein Objekt als Parameter verwenden.

const loginClicked = ({ email, password }) => ({
  type: 'LOGIN_CLICKED',
  payload: { email, password },
});

Wenn Sie die Aktion nun verwenden, sind Sie gezwungen, die Argumente explizit zu benennen.

loginClicked({ email: 'jan@reactsquad.io', password: '5ub5cr1b3' });

Dies macht es offensichtlich, was jedes Argument bedeutet.

Standardwerte

Die Verwendung benannter Parameter über ein Objekt ist auch eine gute Möglichkeit, Standardwerte für die Aktion festzulegen.

const saveProfileAvatarImage = ({ image, alt = 'Your profile avatar' }) => ({
  type: 'SAVE_PROFILE_AVATAR_IMAGE',
  payload: { image, alt },
});

In diesem Beispiel wird der alt Parameter ist optional. Wenn Sie ihn nicht angeben, wird er standardmäßig auf 'Ihr Profil-Avatar' gesetzt. Dadurch erhalten Sie auch Typinferenz für die Aktion, und Ihr Editor weiß, dass die alt Eigenschaft ein String sein sollte.

Datentransformationen

Action-Creator können Daten transformieren. Wenn Sie beispielsweise ein Datumsobjekt erhalten, das nicht serialisierbar ist, können Sie es in einen String umwandeln.

const saveDate = date => ({
  type: 'SAVE_DATE',
  payload: date.toISOString(),
});

Solange der Action-Creator eine reine Funktion bleibt, können Sie Datentransformationen frei durchführen und sogar Array-Methoden wie .map(), .filter(), .reduce(), und andere. Dies gilt auch für die Action-Handler in den Cases Ihres Reducers. Und komplexe Transformationen führen Sie normalerweise in Ihren Reducer-Case-Handlern durch. Ich wollte jedoch darauf hinweisen, dass Sie theoretisch auch reine Datentransformationen innerhalb Ihres Action-Creators durchführen können.

Benannte Eigenschaften in Payloads

In diesem vorherigen Beispiel haben Sie gesehen, dass der Action-Creator das Datum direkt als Payload zurückgibt. Wenn Sie Ihre Redux-App debuggen, sehen Sie daher nur, dass sich ein Datums-String auf dem Objekt befindet, aber Sie wissen nicht, was er bedeutet.

action SAVE_DATE @ 12:34:56.789
  prev state  { ...previousState }
  action      { 
    type: 'SAVE_DATE', 
    payload: '2024-12-14T00:00:00.000Z' 
  }
  next state  { ...nextState }

Sie können es den Leuten ermöglichen, zu verstehen, was das Datum bedeutet, indem Sie der Nutzlast eine benanntes Eigentum.

const saveDate = date => ({
  type: 'SAVE_DATE',
  payload: { currentBackupDate: date.toISOString() },
});

Wenn Sie sich von der Aktion abmelden, sehen Sie, dass die Payload eine Aktuelles Backupdate Eigentum.

action SAVE_DATE @ 12:34:56.789
  prev state  { ...previousState }
  action      { 
    type: 'SAVE_DATE', 
    payload: { currentBackupDate: '2024-12-14T00:00:00.000Z' }
  }
  next state  { ...nextState }


Dadurch wissen Ihre Kollegen — oder Ihr zukünftiges Ich —, dass das Datum in der Payload das Datum ist, an dem das Backup erstellt wurde.

Sie können dies mit benannten Eigenschaften für Ihre Argumente kombinieren, um Ihre Aktionen noch ausdrucksvoller zu gestalten.

const saveDate = ({ currentBackupDate }) => ({
  type: 'SAVE_DATE',
  payload: { currentBackupDate: currentBackupDate.toISOString() },
});

Dies ändert zwar nichts an der Logging-Ausgabe, macht es aber einfacher zu verstehen, was die Argumente bedeuten, wenn Sie die Aktion in Ihrem Code sehen.

Aktionen können mehrere Auswirkungen haben

Ein anderer Mentee von mir hat einen Login-Flow in Redux erstellt. Er bestand aus vier Aktionen: Einloggen geklickt, Vom Benutzer abgerufen, Toast anzeigen und Anmeldung beenden. Er hat das Laden eingestellt, als die Anmeldung gestartet wurde, den Benutzer beim Abrufen festgelegt und die Anmeldung abgeschlossen, als er fertig war. Er zeigte auch einen Toast.

Er hatte einen Reducer, der den Login-Status und das Benutzerprofil handhaben würde.

export const loginClicked = ({ email, password }) => ({
  type: 'LOGIN_CLICKED',
  payload: { email, password },
});
export const userFetched = () => ({ type: 'USER_FETCHED' });
export const finishLogin = () => ({ type: 'FINISH_LOGIN' });

const initialState = {
  email: '',
  password: '',
  isLoading: false,
  user: null,
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case loginClicked().type: {
      return { ...state, isLoading: true };
    }
    case userFetched().type: {
      return { ...state, user: payload };
    }
    case finishLogin().type: {
      return { ...state, isLoading: false };
    }
    default: {
      return state;
    }
  }
};

Dieser Reduzierer war für drei Aktionen verantwortlich: Einloggen geklickt, Vom Benutzer abgerufen, und Anmeldung beenden.

Als auf den Login geklickt wurde, stellte er den Ladezustand auf wahr. Als der Benutzer abgerufen wurde, hat er das Benutzerprofil festgelegt. Und als die Anmeldung abgeschlossen war, stellte er den Ladestatus auf falsch.

Er hatte auch einen Toast-Reducer, der zum Beispiel eine Toast-Meldung anzeigte, wenn die Anmeldung erfolgreich war.

export const showToast = message => ({ type: 'SHOW_TOAST', payload: message });

const initialState = {
  message: '',
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case showToast().type: {
      return { ...state, message: payload };
    }
    default: {
      return state;
    }
  }
};

Auch hier vereinfache ich den Code in all diesen Beispielen, um den Punkt zu veranschaulichen.

Er nutzte all diese Aktionen in seiner Saga.

import { call, put, takeLeading } from 'redux-saga/effects';

import { showToast } from './toast-reducer';
import { login } from './user-authentication-api';
import {
  finishLogin,
  loginClicked,
  userFetched,
} from './user-authentication-reducer';

function* handleLoginClicked({ payload: { email, password } }) {
  const user = yield call(login, email, password);
  yield put(userFetched(user));
  yield put(finishLogin());
  yield put(showToast('Login successful'));
}

export function* watchLoginClicked() {
  yield takeLeading(loginClicked().type, handleLoginClicked);
}

Wenn du aufgepasst hast, wirst du feststellen, dass er schon vieles richtig gemacht hat. Er benannte einige Aktionen anschaulich, was auch zu gut benannten Sagen führte. Außerdem verwendete er dieselben Aktionen an mehreren Stellen wieder — zum Beispiel in Einloggen geklickt Aktion hat sowohl den Zustand geändert als auch gestartet Handle Login angeklickt Saga. Er strukturierte seinen Code jedoch so, dass zu viele Aktionen ausgeführt werden mussten.

Wenn die Anmeldung erfolgreich ist, ist das einzig bedeutsame Ereignis, dass die Anmeldung erfolgreich war. Es ist nicht nötig, die Konsequenzen so detailliert zu gestalten.

Er hat das behoben, indem er eine einzige Aktion erstellt hat, Anmeldung war erfolgreich, das versendet wird, wenn die Anmeldung erfolgreich ist.

export const loginClicked = ({ email, password }) => ({
  type: 'LOGIN_CLICKED',
  payload: { email, password },
});
export const loginSucceeded = user => ({
  type: 'LOGIN_SUCCEEDED',
  payload: { user },
});

const initialState = {
  email: '',
  password: '',
  isLoading: false,
  user: null,
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case loginClicked().type: {
      return { ...state, isLoading: true };
    }
    case loginSucceeded().type: {
      return { ...state, isLoading: false, user: payload.user };
    }
    default: {
      return state;
    }
  }
};

Beachten Sie, wie er das entworfen hat Anmeldung war erfolgreich Aktion mit benannten Eigenschaften, um den Benutzer als Argument zu verwenden und ihn dennoch als Schlüssel-Wert-Paar in einem Objekt in die Nutzlast aufzunehmen. Wie Sie bereits erfahren haben, macht es dieser Ansatz einfacher zu erkennen, was die Payload enthält, wenn Sie Logging-Middleware oder Redux DevTools verwenden.

Jetzt kannst du die Action in der Saga nutzen, um zwei Dispatch-Anrufe zu einem zusammenzufassen.

import { call, put, takeLeading } from 'redux-saga/effects';

import { showToast } from './toast-reducer';
import { login } from './user-authentication-api';
import { loginClicked, loginSucceeded } from './user-authentication-reducer';

function* handleLoginClicked({ payload: { email, password } }) {
  const user = yield call(login, email, password);
  yield put(loginSucceeded(user));
  yield put(showToast('Login successful'));
}

export function* watchLoginClicked() {
  yield takeLeading(loginClicked().type, handleLoginClicked);
}

Mehrere Reduzierstücke können dieselbe Aktion ausführen

Sie haben vielleicht bemerkt, dass eine separate Aktion Toast anzeigen, wird in der Saga immer noch versendet.

Eine weitere wichtige bewährte Methode für Maßnahmen ist, dass Es ist in Ordnung, wenn mehrere Reduzierstücke dieselbe Aktion ausführen. Anstatt zu versenden Toast anzeigen, lassen Sie den Toastreduzierer auch darauf reagieren Anmeldung war erfolgreich:

import { loginSucceeded } from './user-authentication-reducer';

export const showToast = message => ({ type: 'SHOW_TOAST', payload: message });

const initialState = {
  message: '',
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case showToast().type: {
      return { ...state, message: payload };
    }
    case loginSucceeded().type: {
      return { ...state, message: 'Login successful' };
    }
    default: {
      return state;
    }
  }
};

Du importierst die Anmeldung war erfolgreich Aktion von user-authentication-reducer.js und erzeuge ein Argument dafür in toast-reducer.js.

Der Handler für die Anmeldung war erfolgreich Aktion ignoriert die Nutzlast. Sie müssen keine Nachricht als Payload übergeben, da die Toast-Nachricht bei erfolgreicher Anmeldung immer dieselbe ist.

Jetzt können Sie das entfernen Toast anzeigen Action aus der Saga, weil Anmeldung war erfolgreich zeigt implizit den Toast.

import { call, put, takeLeading } from 'redux-saga/effects';

import { login } from './user-authentication-api';
import { loginClicked, loginSucceeded } from './user-authentication-reducer';

function* handleLoginClicked({ payload: { email, password } }) {
  const user = yield call(login, email, password);
  yield put(loginSucceeded(user));
}

export function* watchLoginClicked() {
  yield takeLeading(loginClicked().type, handleLoginClicked);
}

Dieselbe Aktion an mehreren Stellen ausführen

Als wir seinen Code während einer Mentoring-Sitzung so umgestaltet haben, fragte der Mentee:

„Was ist, wenn Anmeldung war erfolgreich Aktion wird an mehreren Stellen verwendet? Zum Beispiel, wenn es verschiedene Sagas für verschiedene Login-Strategien gibt (z. B. Google, E-Mail/Passwort usw.)?“

Die Antwort lautet: wenn das Verhalten der Reduzierer dasselbe sein sollte, können Sie dieselbe Aktion verwenden. Wenn sie unterschiedlich sind, sollten Sie unterschiedliche Aktionen verwenden, die genau beschreiben, was anders passiert ist. Anstatt Anmeldung war erfolgreich, du hättest Die Anmeldung mit Google war erfolgreich und Anmeldung mit E-Mail-Passwort erfolgreich, und so weiter. Dies geht auf den ersten Punkt zurück, bei dem es um die Verwendung beschreibender Aktionsnamen geht. Den Code dafür finden Sie im nächsten Abschnitt.

Zusammenfassend lässt sich sagen, dass eine Aktion entweder in mehreren Sagen oder bei Klicks an verschiedenen Stellen in Ihrer App ausgeführt werden kann. Und diese eine Aktion kann bei verschiedenen Reducern ausgeführt werden und gleichzeitig verschiedene Sagas auslösen.

Mehrere Aktionen, die dieselbe Statusaktualisierung verursachen (mit Statusaktualisierungshelfern)

Gehen Sie zurück zum Beispiel von oben mit verschiedenen Login-Strategien und gehen Sie davon aus, dass es eine E-Mail- und eine Google-Login-Strategie gibt. Beide sollten die gleichen Aktualisierungen des Status der Benutzerauthentifizierung zur Folge haben (den booleschen Wert umdrehen, den Benutzer festlegen), aber es sollten unterschiedliche Toast-Meldungen angezeigt werden.

So sieht der Reducer aus:

export const loginWithEmailClicked = ({ email, password }) => ({
  type: 'LOGIN_WITH_EMAIL_CLICKED',
  payload: { email, password },
});
export const loginWithGoogleClicked = () => ({
  type: 'LOGIN_WITH_GOOGLE_CLICKED',
});
export const loginSucceededWithEmail = user => ({
  type: 'LOGIN_SUCCEEDED_WITH_EMAIL',
  payload: { user },
});
export const loginSucceededWithGoogle = user => ({
  type: 'LOGIN_SUCCEEDED_WITH_GOOGLE',
  payload: { user },
});

const initialState = {
  email: '',
  password: '',
  isLoading: false,
  user: null,
};

const startAuthentication = state => ({ ...state, isLoading: true });

const loginSucceeded = (state, { user }) => ({
  ...state,
  isLoading: false,
  user,
});

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case loginWithEmailClicked().type: {
      return startAuthentication(state);
    }
    case loginWithGoogleClicked().type: {
      return startAuthentication(state);
    }
    case loginSucceededWithEmail().type: {
      return loginSucceeded(state, payload);
    }
    case loginSucceededWithGoogle().type: {
      return loginSucceeded(state, payload);
    }
    default: {
      return state;
    }
  }
};

Je nachdem, welche Strategie der Benutzer ausgewählt hat, gibt es zwei verschiedene Aktionen. Zusätzlich werden zwei Aktionen ausgelöst, wenn eine der Anmeldestrategien erfolgreich ist.

Da sie dasselbe Zustandsupdate auslösen, können die Zustandsupdates in Hilfsfunktionen abstrahiert werden - Authentifizierung starten und loginSucceeded – die dann in den jeweiligen Fällen verwendet werden.

So würden die Sagas diese Aktionen verwenden.

import { call, put, takeLeading } from 'redux-saga/effects';

import { loginWithEmail, loginWithGoogle } from './user-authentication-api';
import {
  loginSucceededWithEmail,
  loginSucceededWithGoogle,
  loginWithEmailClicked,
  loginWithGoogleClicked,
} from './user-authentication-reducer';

/*
Email login
*/

function* handleLoginWithEmailClicked({ payload: { email, password } }) {
  const user = yield call(loginWithEmail, email, password);
  yield put(loginSucceededWithEmail(user));
}

export function* watchLoginWithEmailClicked() {
  yield takeLeading(loginWithEmailClicked().type, handleLoginWithEmailClicked);
}

/*
Google login
*/

function* handleLoginWithGoogleClicked() {
  const user = yield call(loginWithGoogle);
  yield put(loginSucceededWithGoogle(user));
}

export function* watchLoginWithGoogleClicked() {
  yield takeLeading(
    loginWithGoogleClicked().type,
    handleLoginWithGoogleClicked,
  );
}

Sie erstellen zwei Sagas, die auf die jeweiligen Aktionen hören und die entsprechenden Login-Erfolgsaktionen auslösen.

Sie müssen auch für jede Strategie einen Fall zum Toast-Reducer hinzufügen, da sie unterschiedliche Toast-Nachrichten anzeigen sollen.

import {
  loginSucceededWithEmail,
  loginSucceededWithGoogle,
} from './user-authentication-reducer';

export const showToast = message => ({ type: 'SHOW_TOAST', payload: message });

const initialState = {
  message: '',
};

export const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case showToast().type: {
      return { ...state, message: payload };
    }
    case loginSucceededWithEmail().type: {
      return { ...state, message: 'Email login successful' };
    }
    case loginSucceededWithGoogle().type: {
      return { ...state, message: 'Google login successful' };
    }
    default: {
      return state;
    }
  }
};

In diesem Beispiel haben Sie gesehen, dass die loginWithEmailClicked und loginWithGoogleClicked Aktionen die gleiche Zustandsaktualisierung verursachen. Und die loginSucceededWithEmail und loginSucceededWithGoogle Aktionen die gleiche Zustandsaktualisierung in einem Reducer verursachen, aber unterschiedliche Zustandsaktualisierungen in einem anderen Reducer.

Anhängen von type als statische Eigenschaft

Der letzte Punkt, der die Leute verwirrte, war das Auftreten von Aktionen mit einem statischen type Eigenschaft, die so verwendet wird:

// ... in some-reducer.js
const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case someAction.type: {
      // Handle action
    }
  }
};

// ... in some-saga.js
function* watchSomeAction() {
  yield takeEvery(someAction.type, handleSomeAction);
}

Dieses Muster tritt manchmal auf, insbesondere bei Redux Toolkits createAction, das Sie im dritten Artikel dieser Reihe lernen.

Beim manuellen Erstellen von Actions fügen einige Entwickler gerne den type als statische Eigenschaft zur Action hinzu:

const someAction = payload => ({ type: someAction.type, payload });
someAction.type = 'SOME_ACTION';

const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case someAction.type: {
      return { ...state, message: payload };
    }
    // ...
  }
};

function* watchSomeAction() {
  yield takeEvery(someAction.type, handleSomeAction);
}

Dieses Muster hat zwei Hauptvorteile:

  1. Es vermeidet die Erstellung unnötiger Action-Objekte, wenn die Action aufgerufen wird, um den Typ für den Case zu bestimmen, oder beim Einrichten von Sagas.
  2. Wenn Sie Actions mit Transformationen haben, die Daten erfordern, können Sie diese nicht ohne Argumente aufrufen, wie wir es oben in Cases und beim Einrichten von Sagas getan haben.
const explodeWithoutArgs = data => ({
  type: explodeWithoutArgs.type,
  payload: data.toUpperCase(), // This will throw if data is undefined
});
explodeWithoutArgs.type = 'EXPLODE_WITHOUT_ARGS';

const reducer = (state = initialState, { type, payload } = {}) => {
  switch (type) {
    case explodeWithoutArgs.type: {
      return { ...state, message: payload };
    }
    case explodeWithoutArgs().type: { // This would crash ❌
      return { ...state, message: payload };
    }
    // ...
  }
};

Das lässt sich natürlich vermeiden, wenn Sie stattdessen die Daten im jeweiligen Cases-Action-Handler transformieren.

Dieser Unterschied ist gering und stilistisch. Es ist eine Frage der Präferenz.

Redux Toolkit

Alles, was Sie in diesem Artikel gelernt haben, gilt auch beim Erstellen von Slices mit createSlice aus Redux Toolkit (RTK).

Meine letzten Artikel über Redux erhielten etwas negatives Feedback, weil ich Redux ohne RTK zeige und viele Leute die Verwendung von RTK bevorzugen. Ich glaube jedoch, dass viele Grundlagen mit Vanilla Redux leichter zu verstehen sind, da das Erlernen von RTK dann nur noch eine Frage des Erlernens der neuen API-Syntax ist.

Wie dem auch sei, so würde der Toast-Reducer mit RTK aussehen. (Hinweis: Es empfiehlt sich, diesen Artikel in einem anderen Tab zu öffnen und die beiden Versionen des Toast-Reducers nebeneinander zu vergleichen.)

import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';

import {
  loginSucceededWithEmail,
  loginSucceededWithGoogle,
} from './user-authentication-reducer';

export const {
  actions: { showToast },
  reducer,
} = createSlice({
  name: 'auth',
  initialState: {
    message: '',
  },
  reducers: {
    showToast(state, action: PayloadAction<string>) {
      state.message = action.payload;
    },
  },
  extraReducers: builder => {
    builder
      .addCase(loginSucceededWithEmail, state => {
        state.message = 'Email login successful';
      })
      .addCase(loginSucceededWithGoogle, state => {
        state.message = 'Google login successful';
      });
  },
});

Sie können die createSlice Funktion verwenden, um Ihre Reducer zu erstellen.

Sie definieren den showToast Action Creator im reducers Objekt.

Und Sie reagieren auf die loginSucceededWithEmail und loginSucceededWithGoogle Aktionen im extraReducers Objekt.

Die Syntax von Redux Toolkit wird im dritten Artikel dieser Redux-Reihe ausführlich erklärt, lesen Sie diesen also als Nächstes.

Wenn Sie Leiter eines Entwicklungsteams sind und Redux-Entwickler einstellen möchten, Termin vereinbaren mit uns und werden Sie innerhalb von 24 Stunden vermittelt.

Hire reliable React Developers without breaking the bank
Match me with a dev
About the Author
Jan Hesters
CTO, ReactSquad
What's up, this is Jan, CTO of ReactSquad. After studying physics, I ventured into the startup world and became a programmer. As the 7th employee at Hopin, I helped grow the company from a $6 million to a $7.7 billion valuation until it was partly sold in 2023.

Get actionable tips from the ReactSquad team

5-Minute Read. Every Tuesday. For Free