10. März 2017
Denny Israel
0

React + Redux + HATEOAS

When developing a web application with ReactJS it is often a good idea to use it in combination with the Redux architecture. This gives us good and precise control about the application state and all actions that change the state. All view changes must be executed as actions on the redux store and thus the central application state. When dealing with a RESTful backend we have to cope with asynchronous requests and thus have to deal with long running requests, error responses, missing data. Furthermore when the backend uses HATEOAS we have to deal with getting and managing the necessary request links.

HTTP requests with axios

When dispatching actions to the redux store each action triggers several reducers which change the application state. When calling a RESTful backend these reducers would have to call the backend and wait for the response. But doing an http call is a side effect and thus violates the requirement that the reducers have to be pure functions (https://en.wikipedia.org/wiki/Pure_function). We could call the backend when creating the action and dispatch different actions depending on the outcome of the call (success or fail). Luckily we can use a library that does it for us. With the help of axios – a promised based http client – we can concentrate on creating the request action and the reducers that deal with the respective outcome. Axios offers a redux middleware that handles the execution of the request based on a request action and automatically dispatches success or fail actions. In our first example we search for leads with a search term and save the results in the redux store. First of all we create the axios client and register the axios middleware in the store.

import {applyMiddleware, createStore, compose} from "redux";
import axios from "axios";
import axiosMiddleware from "redux-axios-middleware";

const client = axios.create({
    responseType: "json",
    timeout: 10000,
});
const store = createStore(
    appState,
    compose(applyMiddleware(axiosMiddleware(client))),
);

Now we can dispatch a request action that instructs axios to do an http call to fetch the leads.

dispatch({
    type: GET_LEADS,
    payload: {
        request: {
            url: leadsUrl,
            params: {
                query: searchTerm,
            },
            headers: {
                Authorization: `bearer secret-token`,
            },
        },
    },
})

The actions must specify the type (GET_LEADS, defined by redux) and the payload (defined by axios). The axios middleware recognizes the structure and fetches the leads. When successful (typically http status 2xx), axios dispatches an action with the type GET_LEADS_SUCCESS. When not successful axios dispatches an action with the type GET_LEADS_FAIL.

Now we can implement our reducers which will change the state based on the request actions, such as setting a loading state and of course some changes according to the response actions (…_SUCCESS and …_FAIL) like storing the results in the state or setting an error message.

Dealing with Links (HATEOAS)

We now know how the trigger http requests and how to apply the results to the redux store. But where do we get the request URL from? When dealing with a backend that uses HATEOAS, these links should not be contructed by the client but instead be read from the backend. The client sends a request to the base URL of the backend to get the possible links from. All resources that are available from the root are listed with their respective links. If there are sub resources then their respective links are contained in the answer when fetching a root resource.

react-redux-hateoas-ablauf-saxonia-systems

Workflow of retrieving the leads URL and searching for leads.

To get the URL for the leads (/api/search/companies/leads) we need to fetch the result of the base URL (/api/search) which contains the link to the leads search. To execute a search we have to call the base URL and when this call returns successfully, fetch the leads by calling the search URL contained in the answer. Now we have the problem that axios dispatches the _SUCCESS action of the base URL request directly to the store and we cannot dispatch another action in the reducer as this would make the reducer impure (https://en.wikipedia.org/wiki/Pure_function#Impure_functions). Luckily the axios middleware returns a promise when dispatching a request action to the store. When the request returns, the promise resolves with the generated success or fail action as a parameter. Now we can create and dispatch other actions based on the outcome of the first action.

dispatch({
 type: GET_SEARCH,
 payload: {
  request: {
   url: "/api/search/",
   headers: {
    Authorization: `bearer secret-token`,
   },
  },
 },
}).then(a => {
 if (a.type === "GET_SEARCH_SUCCESS") {
  const retrievedLeadsUrl = a.payload.data._links.leads.href;
  return dispatch({
   type: GET_LEADS,
   payload: {
    request: {
     url: retrievedLeadsUrl,
     params: {
      query: searchTerm,
     },
     headers: {
      Authorization: ` bearer secret-token`,
     },
    },
   },
  });
 } else {
  return Promise.resolve();
 }
})

This approach requires the explicit use of the dispatch function which is available at the component level (map-dispatch-to-props function). We could create plain action objects and control all action flows in the components or centralize them in action creator functions. Redux offers another approach by using the thunk middleware (https://www.npmjs.com/package/redux-thunk). With the help of redux-thunk we can dispatch functions that take the dispatch function as a parameter and control the usage of the dispatch function. With this approach we can hide the control flow of the action dispatching within the actions themselves.

At first we have to apply the thunk middleware:

import {applyMiddleware, createStore, compose} from "redux";
import axios from "axios";
import axiosMiddleware from "redux-axios-middleware";
import thunk from "redux-thunk";

const client = axios.create({
    responseType: "json",
    timeout: 10000,
});
const store = createStore(
    appState,
    compose(applyMiddleware(thunk, axiosMiddleware(client))),
);

An action creation function would look like this:

export const getLeads = (searchTerm, clintrToken) => (dispatch) => {
    return dispatch({
        type: GET_SEARCH,
        payload: {
            request: {
                url: "/api/search/",
                headers: {
                    Authorization: `bearer ${clintrToken}`,
                },
            },
        },
    }).then(a => {
        if (a.type === "GET_SEARCH_SUCCESS") {
            const retrievedLeadsUrl = a.payload.data._links.leads.href;
            return dispatch({
                type: GET_LEADS,
                payload: {
                    request: {
                        url: retrievedLeadsUrl,
                        params: {
                            query: searchTerm,
                        },
                        headers: {
                            Authorization: `bearer ${clintrToken}`,
                        },
                    },
                },
            });
        } else {
            return Promise.resolve();
        }
    });
};

The function getLeads returns an action for retrieving the leads (action creator). This action is not a plain object but a function which takes a dispatch function and thus allows the action to control the execution of the dispatch function. The action can be dispatched like this:

dispatch(getLeads("Java", "my-secret-token"));

Conclusion

We saw:

  • the problems when calling a backend via http (long running call, error responses, erroneous contents,…)
  • the problem of asynchronous actions in a redux architecture
  • how to deal with asynchronous http calls in a redux architecture
  • how to deal with root resources and fetch links to them
  • how to deal with actions that trigger new actions

Denny is a Senior Consultant for software development in Java and JavaFX. He is primarily concerned with all aspects of the development of JavaFX applications, from the surface through the architecture to the backend and the connection to other systems. Additionally he is interested in modern web technologies like React and Angular 2.

Twitter Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0