Bernd das Brot: React-SPA: best practice, um mit Aktionen nach einer Action umzugehen?

Beitrag lesen

Leider gibt es aber auch das Problem, dass ich Fehler, die von der API kommen, nicht mitbekomme. Das durch Redux-Thunk zurück gegebene Promise läuft nie in den Catch-Fall, es löst nur immer die then-Aktion aus.

Wenn ich mich nicht täusche hat das Problem letztlich mit redux-thunk wenig zu tun. Was du beschreibst ist das Standard-Verhalten von Promises:

return axios.post("/foos", { foo })
  .then(
    result => dispatch(createFooSuccess(result)),
    error => dispatch(createFooFailure(error))
  );

Du bekommst einen Promise von axios.post() und "catcht" den Fehler durch Angabe einer onRejected-Funktion. Dadurch gilt der Fehler erst einmal als behandelt, als wäre er nicht passiert.

Die onRejected-Funktion kann einen Alternativwert zurückgeben. Damit wird der neue, von then() zurückgegebene Promise resolved. Wenn dispatch und createFooFailure keine Exceptions auslösen, so wird der neue Promise immer resolved.

Um das Verhalten zu erklären bietet es sich an, das synchrone Pendant zu Promises zu betrachten: try-catch. Was du machst ist äquivalent zu:

let result;
try {
  result = axios.post(...);
  dispatch(createFooSuccess(result));
} catch (e) {
  dispatch(createFooFailure(error));
}
// Weiterer Code, der annimmt, dass result gefüllt ist

Der "weitere Code" wird hier immer ausgeführt, auch wenn der POST fehlgeschlagen ist.

Wie kann man das verhindern? Indem man den Fehler nach der Behandlung noch einmal mit "throw" auslöst (rethrow).

let result;
try {
  result = axios.post(...);
  dispatch(createFooSuccess(result));
} catch (e) {
  dispatch(createFooFailure(error));
  throw error;
}
// Weiterer Code, der jetzt nur ausgeführt wird, wenn der POST erfolgreich war.

Bei Promises ist es ähnlich: Wenn man will, dass der neue Promise rejected wird, muss man im onRejected den "gefangenen" Fehler noch einmal "werfen".

function createFoo(foo) {
  return dispatch => {
    return axios.post("/foos", { foo })
      .then(
        result => dispatch(createFooSuccess(result)),
        error => {
          dispatch(createFooFailure(error));
          throw error;
        }
      );
  };
}

Jetzt ist der neue, von then() erzeugte Promise im Erfolgsfalle resolved und im Fehlerfalle rejected.

Wenn du jetzt in einem anderen Action Creator obigen benutzt:

dispatch(createFoo(foo)).then(
  () => {
    dispatch(anotherActionCreator());
  }
);

Dann wird anotherActionCreator nur aufgerufen, wenn createFoo erfolgreich war.

Dieses "Rethrowing" kann man natürlich kapseln, sodass man es nicht ausdrücklich schreiben muss und nicht vergessen kann.

Ich könnte natürlich auch meine Redirections und dergleichen in die Actions verlagern und damit die Business-Logik komplett raushalten aus meinen Components… leider fehlt mir da noch etwas die Erfahrung um sagen zu können, was denn der Weg mit den wenigsten Nachteilen ist, um mit diesem Problem umzugehen.

Das ist der Weg, den ich empfehlen würde. Diese Logik ist m.M.n. in den Actions besser aufgehoben und dort auch einfacher zu testen. Durch redux-thunks Promise-Mechanismus können Async Action Creators einfach aufeinander aufbauen.

Die Komponente muss dann auch einen High-Level Action Creator als prop bekommen.

Bernd das Brot