15. Using External Data

15.1. Creating A Simple Backend

To persist our data we will create a backend to fetch our initial data. We will use express to create a simple server. To install type:

$ yarn add express

Now we will create a simple server in the file server.js

 1const express = require("express");
 2const app = express();
 3const port = 3001;
 4
 5const faq = [
 6  {
 7    question: "What does the Plone Foundation do?",
 8    answer: "The mission of the Plone Foundation is to protect and..."
 9  },
10  {
11    question: "Why does Plone need a Foundation?",
12    answer: "Plone has reached critical mass, with enterprise..."
13  }
14];
15
16app.get("/", (req, res) => {
17  res.header("Access-Control-Allow-Origin", "*");
18  res.json(faq);
19});
20
21app.listen(port, () => console.log(`Listening on port: ${port}`));

Then we can run our newly created server:

$ node server.js

Now it is time to write our action to fetch the items from the backend in the file actions/index.js:

19export const getFaqItems = () => ({
20  type: "GET_FAQ_ITEMS",
21  request: {
22    op: "get",
23    path: "/"
24  }
25});

15.2. Writing Middleware

Since the action itself doesn’t do any api call we will create middleware to do the job. Redux middleware is a simple method which receives the store, the method to call the next action and the action itself. The middleware can then decide to do something based on the data in the action. In our case we are looking for a property called request. If that one is available we want to do an api call with the provided operation, path and data and fire a new action when the data is fetched. We will create a file at middleware/api.js and the implementation will look like this:

 1export default store => next => action => {
 2  const { request, type, ...rest } = action;
 3
 4  if (!request) {
 5    return next(action);
 6  }
 7
 8  next({ ...rest, type: `${type}_PENDING` });
 9
10  const actionPromise = fetch(`http://localhost:3001${request.path}`, {
11    method: request.op,
12    body: request.data && JSON.stringify(request.data)
13  });
14
15  actionPromise.then(response => {
16    response.json().then(data => next({ data, type: `${type}_SUCCESS` }));
17  });
18
19  return actionPromise;
20};

Finally we need to apply our middleware to the store in App.js:

 1import React, { Component } from "react";
 2import { Provider } from "react-redux";
 3import { createStore, applyMiddleware } from "redux";
 4
 5import rootReducer from "./reducers";
 6import Faq from "./components/Faq";
 7import api from "./middleware/api";
 8
 9import "./App.css";
10
11const store = createStore(rootReducer, applyMiddleware(api));
12
13class App extends Component {
14  render() {
15    return (
16      <Provider store={store}>
17        <Faq />
18      </Provider>
19    );
20  }
21}
22
23export default App;

Differences

--- a/src/App.js
+++ b/src/App.js
@@ -1,13 +1,14 @@
import React, { Component } from "react";
import { Provider } from "react-redux";
-import { createStore } from "redux";
+import { createStore, applyMiddleware } from "redux";

import rootReducer from "./reducers";
import Faq from "./components/Faq";
+import api from "./middleware/api";

import "./App.css";

-const store = createStore(rootReducer);
+const store = createStore(rootReducer, applyMiddleware(api));

class App extends Component {
  render() {

Last part is to change our reducer at reducers/faq.js to handle the GET_FAQ_ITEMS_SUCCESS action:

 1const faq = (state = [], action) => {
 2let faq;
 3switch (action.type) {
 4  case "ADD_FAQ_ITEM":
 5    return [
 6      ...state,
 7      {
 8        question: action.question,
 9        answer: action.answer
10      }
11    ];
12  case "EDIT_FAQ_ITEM":
13    faq = [...state];
14    faq[action.index] = {
15      question: action.question,
16      answer: action.answer
17    };
18    return faq;
19  case "DELETE_FAQ_ITEM":
20    faq = [...state];
21    faq.splice(action.index, 1);
22    return faq;
23  case "GET_FAQ_ITEMS_SUCCESS":
24    return action.data;
25  default:
26    return state;
27  }
28};
29
30export default faq;