Blog

Wie wir Redux mit unserer Services API verbinden

Januar 24, 2018
Haftungsausschluss: Dieser Artikel setzt ein funktionierendes Verständnis von Javascript und Redux voraus.

Wir möchten Ihnen zeigen, wie wir unsere React- und Redux-Anwendung mit unserer Services-API verbinden. Es war eine interessante Reise und wir sind zufrieden mit dem, was wir bisher haben.

Zuallererst müssen Sie verstehen, dass wir die Authentifizierung mit JWT verwenden. Nach einer ersten Anfrage, bei der wir das Token erhalten, speichern wir es im Redux-Status. Bei allen nachfolgenden Anfragen müssen wir das Token abrufen und es zu den Headern der Anfrage hinzufügen.

Erster Ansatz

Unser erster Ansatz zur Lösung des Problems war die Verwendung einer benutzerdefinierten Redux-Middleware.

Die Idee ist, eine Aktion mit einem Objekt zu versenden, das dem in $.ajax verwendeten Konfigurationsobjekt ähnelt. Dieses Objekt wird dann von der Middleware abgefangen und führt die Anfrage aus.

So könnte ein Action Creator aussehen:

const getFiles = () => ({
type: API,
path: ‘/files’,
onSuccess: addFiles,
});

Dabei ist addFiles ein weiterer Action Creator, der mit der Antwort auf den Aufruf von /files versendet wird.

const addFiles = (files) => ({
type: ADD_FILES,
payload: files,
});

Die Middleware ist dafür verantwortlich, das Benutzer-Token aus dem Redux-Status zu lesen, die Header damit zu erstellen und eine Anfrage über die Pfadeigenschaft in der Aktion zu stellen.

Dies ist ein einfaches Beispiel dafür, wie die Middleware aussehen könnte:

const middleware = ({ dispatch, getState }) => (next) => (action) => {
if (action.type !== API) {
return next(action);
}
const token = getState().user.token;
const headers = new Headers({
Authorization: `Bearer ${token}`,
});
const url = `https://www.someurl.com/api${action.path}`;
return fetch(url, { headers })
.then((res) => res.json())
.then((data) => dispatch(action.onSuccess(data)));
}

Probleme mit dem ersten Ansatz

Die benutzerdefinierte Middleware funktionierte für einfache Interaktionen mit der API sehr gut. Bei komplexeren Abläufen wurde es jedoch immer schwieriger. In einem bestimmten Ablauf wird nach dem Hochladen einer Datei immer wieder unsere Dienste-API abgefragt, um sicherzustellen, dass der Upload in S3 zufriedenstellend war.

Das bedeutet, dass wir nach der ersten Anfrage eine Anfrage nach der anderen stellen wollen, bis eine Antwort vorliegt oder wir eine maximale Anzahl von Wiederholungsversuchen erreichen. Dieser Ablauf ist nicht sehr komplex. Wir haben jedoch festgestellt, dass er mit unserer benutzerdefinierten Middleware bereits schwer zu lesen ist.

Es ist nicht nötig, auf die Einzelheiten einzugehen, wie wir dies mit der benutzerdefinierten Middleware gelöst haben. Aber es war keine elegante Lösung.

Zurück zu den Grundlagen

Uns wurde klar, dass wir die Antwort schon die ganze Zeit vor der Nase hatten. Wir haben redux-thunk verwendet. Das war unsere Antwort. Wir konnten eine asynchrone Funktion verwenden, um das Polling in einen synchronen Code mit einer while-Schleife und await umzuwandeln.

const getFiles = () => async ({ dispatch, getState }) => {
const token = getState().user.token;
const headers = new Headers({
Authorization: `Bearer ${token}`,
});
const url = ‘https://www.someurl.com/api/files’;
const response = await fetch(url, { headers });
const files = await res.json();
const finalAction = addFiles(files);
return dispatch(finalAction);
}

Denken Sie daran, dass wir bei jeder Anfrage auch das Token zum Header hinzufügen müssen. Das in jeder Thunk Action zu machen ist keine sehr elegante Lösung. Auch nicht, um die ganze URL zu haben.

Lösung: Modul Dienstleistungen

Uns wurde klar, dass wir ein Modul für die Kommunikation mit der Dienste-API benötigten.

Wir wollten ein Modul, das auf folgende Weise verwendet werden kann:

const getFiles = () => async ({ dispatch }) => {
const response = await services.getFiles();
const files = await res.json();
const finalAction = addFiles(files);
return dispatch(finalAction);
};

Diese Aktion ist einfach und leicht zu lesen. Aber noch wichtiger ist, dass diese Thunk-Aktionen es uns ermöglichen, komplexe Abläufe einfach zu erstellen. Die Abfrage könnte zum Beispiel so aussehen:

const uploadFile = (file) => async ({ dispatch }) => {
const response = await services.getUploadUrl(file);
const { newFile, uploadUrl } = await response.json():
await uploadFile(file, uploadUrl);
let retries = 0;
let exists = false;
while (!exists && retries
exists = await services.fileExists(newFile.id);
retries += 1;
}
if (!exists) {
return dispatch(errorNotification(‘File did not upload successfully, please try again’))
}
return dispatch(addFile(newFile));
};

Die uploadFile-Aktion ist genauso einfach zu lesen wie die vorherige getFiles-Aktion, auch wenn der Fluss komplexer ist, aber wir müssen immer noch herausfinden, wo das Token aus dem Redux-Status gelesen wird.

Injektion von Abhängigkeiten

Wir befinden uns im Browser. Das bedeutet, dass unser Servicemodul Fetch verwenden wird, um die Anfragen zu stellen.

const services = {
getFiles: () => {
const url = ‘https://www.someurl.com/api/files’;
return fetch(url);
},
};

Jedes Mal, wenn wir fetch verwenden, wollen wir auf den Status zugreifen, das Token abrufen und die Kopfzeile erstellen.

Lassen Sie uns unsere eigene Version von fetch erstellen.

const myFetch = (url, configuration) => {
return fetch(url, configuration)
};

Verwenden wir eine Factory-Funktion, um unser myFetch zu erstellen.

const myFetchFactory = () => (url, configuration) => {
return fetch(url, configuration)
};
const myFetch = myFetchFactory();

Warum tun wir das? Gute Frage. Lesen Sie weiter.

Wir können den Redux Store an die Factory übergeben.

import appStore from 'store';

const myFetchFactory = (store) => (url, configuration) => {
return fetch(url, configuration)
};
const myFetch = myFetchFactory(appStore);

Jetzt haben wir alles, was wir brauchen. In unserer myFetchFactory können wir das Token aus dem Redux-Status abrufen und den Header erstellen.

const myFetchFactory = (store) => (url, configuration) => {
const token = store.getState().user.token;
const headers = new Headers({
Authorization: `Bearer ${token}`,
});
const newConfiguration = {
…configuration,
headers,
};
return fetch(url, newConfiguration);
};

const myFetch = myFetchFactory(store);
export default myFetch;

Schließlich injizieren wir unser myFetch, das in unseren Servicemodulen anstelle des Standard-Fetches verwendet werden soll.

// Import our dependency
import myFetch from ‘./myFetch’;

const servicesFactory = (myFetch) => ({
getFiles: () => {
const url = ‘https://www.someurl.com/api/files’;
return myFetch(url);
},
});

const services = servicesFactory(myFetch);

Schlussfolgerung

Bei komplexen asynchronen Abläufen sollten Sie zur besseren Lesbarkeit async und await verwenden. Um auf den Store in Modulen zuzugreifen, die weder Thunk Actions noch Redux Middleware sind, übergeben Sie den Store einfach beim Initialisieren des Moduls. Zögern Sie nicht, Ihre Kommentare unten zu hinterlassen. Wir würden gerne hören, wie andere dieses Problem gelöst haben und ihre Best Practices teilen. Schauen Sie sich auch die offenen Stellen an, wenn dieser Blogbeitrag Sie anspricht.