Adding AJAX calls
So far, we have only been dealing with static data in our Flux flow. The time has now come to add real data connections to the flow and thereby real data. It is time to start talking to APIs through AJAX and HTTP. Fetching data is quite easy nowadays, thanks to the fetch API and libraries such as RxJS. What you need to think about when incorporating it in the flow is:
- Where to place the HTTP call
- How to ensure that the store is updated and interested views are notified
We have a point at which we register the store to the dispatcher, with this piece of code:
// excerpt from store-actions-immutable.js
const createProduct = (product) => {
if (!store["products"]) {
store["products"] = [];
}
store.products = [...store.products, Object.assign(product)];
}
dispatcher.register(({ type, data }) => {
switch (type) {
case CREATE_PRODUCT:
createProduct(data);
store.emitChange();
break;
/* other cases below */
}
})
If we do this for real, that is, call an API to persist this product, createProduct() would be where we would do the API call, like so:
// example use of fetch()
fetch(
'/products' ,
{ method : 'POST', body: product })
.then(response => {
// send a message to the dispatcher that the list of products should be reread
}, err => {
// report error
});
Calling fetch() returns a Promise. Let's use async/await however, as it makes the call much more readable. The difference in code can be seen in the following example:
// contrasting example of 'fetch() with promise' vs 'fetch with async/await'
fetch('url')
.then(data => console.log(data))
.catch(error => console.error(error));
// using async/await
try {
const data = await fetch('url');
console.log(data);
} catch (error) {
console.error(error);
}
Replacing what happens in createProduct() with this adds code with a lot of noise so it is a good idea to wrap your HTTP interactions in an API construct like so:
// api.js
export class Api {
createProduct(product) {
return fetch("/products", { method: "POST", body: product });
}
}
Now let us replace the createProduct() method content with the call to our API construct like so:
// excerpt from store-actions-api.js
import { Api } from "./api";
const api = new Api();
createProduct() {
api.createProduct();
}
That's not really enough though. Because we created a product through an API call, we should dispatch an action that forces the product list to be reread. We don't have such an action or supporting method in a store to handle it, so let's add one:
// product.constants.js
export const SELECT_INDEX = "SELECT_INDEX";
export const CREATE_PRODUCT = "CREATE_PRODUCT";
export const REMOVE_PRODUCT = "REMOVE_PRODUCT";
export const GET_PRODUCTS = "GET_PRODUCTS";
Now let's add the required method in the store and the case to handle it:
// excerpt from store-actions-api.js
import { Api } from "./api";
import {
// other actions per usual
GET_PRODUCTS,
} from "./product.constants";
const setProducts = (products) => {
store["products"] = products;
}
const setError = (error) => {
store["error"] = error;
}
dispatcher.register( async ({ type, data }) => {
switch (type) {
case CREATE_PRODUCT:
try {
await api.createProduct(data);
dispatcher.dispatch(getProducts());
} catch (error) {
setError(error);
storeInstance.emitError();
}
break;
case GET_PRODUCTS:
try {
const products = await api.getProducts();
setProducts(products);
storeInstance.emitChange();
}
catch (error) {
setError(error);
storeInstance.emitError();
}
break;
}
});
We can see that the CREATE_PRODUCT case will call the corresponding API method createProduct(), which on completion will dispatch the GET_PRODUCTS action. The reason for doing so is that when we successfully manage to create a product, we need to read from the endpoint to get an updated version of the products list. We don't see that in detail, but it is being invoked through us calling getProducts(). Again, it is nice to have a wrapper on everything being dispatched, that wrapper being an action creator.
The full file looks like this:
// store-actions-api.js
import dispatcher from "./dispatcher";
import { Action } from "./api";
import { Api } from "./api";
import {
CREATE_PRODUCT,
GET_PRODUCTS,
REMOVE_PRODUCT,
SELECT_INDEX
} from "./product.constants";
let store = {};
class Store extends EventEmitter {
constructor() {}
addListener(listener) {
this.on("changed", listener);
}
emitChange() {
this.emit("changed");
}
emitError() {
this.emit("error");
}
getSelectedItem() {
return store["selectedItem"];
}
}
const api = new Api();
const storeInstance = new Store();
const selectIndex = index => {
store["selectedIndex"] = index;
};
const createProduct = product => {
if (!store["products"]) {
store["products"] = [];
}
store.products = [...store.products, Object.assign(product)];
};
const removeProduct = product => {
if (!store["products"]) return;
store["products"] = products.filter(p => p.id !== product.id);
};
const setProducts = products => {
store["products"] = products;
};
const setError = error => {
store["error"] = error;
};
dispatcher.register(async ({ type, data }) => {
switch (type) {
case "SELECT_INDEX":
selectIndex(message.data);
storeInstance.emitChange();
break;
case CREATE_PRODUCT:
try {
await api.createProduct(data);
storeInstance.emitChange();
} catch (error) {
setError(error);
storeInstance.emitError();
}
break;
case GET_PRODUCTS:
try {
const products = await api.getProducts();
setProducts(products);
storeInstance.emitChange();
} catch (error) {
setError(error);
storeInstance.emitError();
}
break;
}
});