Integrating Redux with SUSI.AI Web Clients

In this blog post, we are going to go through the implementation of the Redux integration on the SUSI.AI web clients. The existing SUSI.AI WebChat codebase has Flux integrated into it, but integrating Redux would make it a lot easier to manage the app state in a single store. And would result in a more maintainable and performant application. Let us go through the implementation in the blog –

The key steps involved the following –

  • Restructuring the directory structure of the repository to enable better maintenance.
  • Creating a Redux store and configuring the middlewares.
  • Standardizing the format for writing actions and make API calls on dispatching an action.
  • Standardizing the format for writing reducers.
  • Hook the components to the Redux store.

Restructuring the directory structure

DIrectory structure for https://chat.susi.ai
  • All the redux related files and utils are put into the redux directory, to avoid any sort of confusion, better maintenance and enhanced discoverability. The prime reason for it also because the integration was done side-by-side the existing Flux implementation.
  • The actions and reducers directory each has a index.js, which exports all the actions and reducers respectively, so as to maintain a single import path for the components and this also helped to easily split out different types of actions/reducers.

Creating Redux store and configure middlewares

import { createStore as _createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import reduxPromise from 'redux-promise';
import reducers from './reducers';

export default function createStore(history) {
 // Sync dispatched route actions to the history
 const reduxRouterMiddleware = routerMiddleware(history);
 const middleware = [reduxRouterMiddleware, reduxPromise];

 let finalCreateStore;
 finalCreateStore = applyMiddleware(...middleware)(_createStore);

 const store = finalCreateStore(
   reducers,
   {},
   window.__REDUX_DEVTOOLS_EXTENSION__ &&
     window.__REDUX_DEVTOOLS_EXTENSION__(),
 );

 return store;
}
  • The function createStore takes in the browserHistory (provided by React Router) and returns a single store object that is passed on to the entry point component of the App.
  • The store is passed to the application via the <Provider> component, provided by the react-redux. It is wrapped to the <App> component as follows –

ReactDOM.render(
 <Provider store={store} key="provider">
   <App />
 </Provider>,
 document.getElementById('root'),
);
  • The 2 middlewares used are routerMiddleware provided by the react-router-redux and the reduxPromise provided by redux-promise.
  • The routerMiddleware enhances a history instance to allow it to synchronize any changes it receives into application state.
  • The reduxPromise returns a promise to the caller so that it can wait for the operation to finish before continuing. This is useful to assist the application to tackle async behaviour.

Standardizing the actions and making API calls on action dispatch

  • The actions file contains the following –

import { createAction } from 'redux-actions';
import actionTypes from '../actionTypes';
import * as apis from '../../apis';

const returnArgumentsFn = function(payload) {
 return Promise.resolve(payload);
};

export default {
// API call on action dispatch
 getApiKeys: createAction(actionTypes.APP_GET_API_KEYS, apis.fetchApiKeys),
// Returns a promise for actions not requiring API calls
 logout: createAction(actionTypes.APP_LOGOUT, returnArgumentsFn),
};

  • As new actions are added, it can be added to the actionTypes file and can be added in the export statement of the above snippet. This enables very standard and easy to manage actions.
  • This approach allows to handle both types of action dispatch – with and without API call. In case of API call on dispatch, the action resolves with the payload of the API.
  • The APIs are called via the AJAX helper (Check out this blog – https://blog.fossasia.org/make-a-helper-for-ajax-requests-using-axios/ ).

Standardizing the reducers and combining them

  • The reducers file contains the following –

import { handleActions } from 'redux-actions';
import actionTypes from '../actionTypes';

const defaultState = {
 ...
 apiKeys: {},
 ...
};

export default handleActions({
  [actionTypes.APP_GET_API_KEYS](state, { payload }) {
     const { keys } = payload;
     return {
       ...state,
       apiKeys: { ...keys },
     };
  },
  ...
},defaultState);
  • The default application state is defined and the reducer corresponding to each action type returns an immutable object which is the new application state,
  • In the above example, the payload from the getApiKeys API call is received in the reducer, which then updated the store.
  • The reducers/index.js  combines all the reducers using combineReducers provided by redux and exports a single object of reducers.

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import app from './app';
import settings from './settings';
import messages from './messages';

export default combineReducers({
 routing: routerReducer,
 app,
 settings,
 messages,
});

Hook the components to the Redux store

  • After Redux integration and standardization of the reducers, actions, any of the component can be hooked to the store as follows –

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

class MyComponent extends Component {
 componentDidMount() {
    // Dispatch an action
    this.props.actions.getApiKeys();
 }
 render() {
   const { apiKeys } = this.props;
   return (
     <div>
        /* JSX */
     </div>     
   );
 }
}

function mapStateToProps(store) {
 const { apiKeys } = store.app;
 return {
   apiKeys
 };
}

function mapDispatchToProps(dispatch) {
 return {
   actions: bindActionCreators(actions, dispatch),
 };
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
  • The mapStateToProps is a function that is used to provide the store data to the component via props, whereas mapDispatchToProps is used to provide the action creators as props to the component.

The above was the implementation of Redux in the SUSI.AI Web Clients. I hope the blog provided a detailed insight of how Redux was integrated and the standards that were followed.

References

Continue ReadingIntegrating Redux with SUSI.AI Web Clients

Make a helper for AJAX requests using axios

In this blog post, we are going to go through the implementation of the helper function that is created for making AJAX requests using axios. Currently, the AJAX calls are made in a very random manner where the calls are written in the component itself and involves rewriting of headers, etc for the API call. Let us go through the implementation in the blog, which will standardise the way to make API calls in SUSI.AI web clients.

The primary changes are –

  • Making a common helper for AJAX requests with the help of axios.
  • Making a common file containing all the API calls across the project.

Going through the implementation

The API calls within the repository were not being made in an organised way, also a lot of redundant code was present. The aim of creating the helper is that, all the API calls is called via this common function. It takes care of the headers and also sending access_token with the API if the user is already logged in for API calls requiring authentication. The function for a API request now looks this simple –

// API call for signup
export function getSignup(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/signup.json`;
 return ajax.get(url, { signup: email, password });
}
  • In the above snippet, the ajax is the common helper used for making API calls. ajax is an object of functions that returns a promise for  various methods of API requests
  • We have primarily taken into consideration GET & POST requests type.
  • The helper function is as follows –
/* Insert imports here*/
const cookies = new Cookies();
const obj = {};

['get', 'post', 'all'].forEach(function(method) {
 obj[method] = function(url, payload, settings = {}) {
   /* Request will be aborted after 30 seconds */
   settings = {
     timeout: 30000,
     dataType: 'json',
     crossDomain: true,
     ...settings,
   };
   
   // Check if logged in
   if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }

   return new Promise(function(resolve, reject) {
     let methodArgs = [];
     if (method === 'post') {
       if (payload && payload instanceof FormData !== true) {
           // Convert to Form Data
           payload = toFormData(payload);
       }

       settings.headers = {
         'Content-Type': 'application/x-www-form-urlencoded',
         ...settings.headers,
       };
     } else if (method === 'get') {
       if (payload) {
         // Add params to the URL   
         url += `?${Object.keys(payload)
           .map(key => key + '=' + payload[key])
           .join('&')}`;
       }
     }

     const methodsToAxiosMethodsMap = {
       get: 'get',
       post: 'post',
       all: 'all',
     };

     if (method === 'all') {
       methodArgs = [url];
     } else if (method === 'get') {
       methodArgs = [url, settings];
     } else {
       methodArgs = [url, payload, settings];
     }

     axios[methodsToAxiosMethodsMap[method]].apply({}, methodArgs).then(
       function(data = {}, ...restSuccessArgs) {
         const statusCode = _.get(data, 'status');
         /*  Send only api response */
         let responseData = { statusCode, ..._.get(data, 'data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise resolved and return the payload
         resolve(camelizeKeys(responseData), ...restSuccessArgs);
       },
       function(data = {}, ...restErrorArgs) {
         // If request is canceled by user
         if (axios.isCancel(data)) {
           reject(data);
         }

         const statusCode = _.get(data, 'response.status', -1);
         let responseData = { statusCode, ..._.get(data, 'response.data') };

         if (method === 'all') {
           responseData = data;
           responseData.statusCode = statusCode;
         }

         if (payload) {
           responseData.requestPayload = payload;
         }
         // Mark the promise rejected and return the payload
         reject(camelizeKeys(responseData), ...restErrorArgs);
       },
     );
   });
 };
});

export default obj;
  • The above objects contains 3 functions –
    • ajax.get(url, payload, settings) – GET – The helper adds the query params to the URL by iterating through them and appending it to the URL and joining them with &.
    • ajax.post(url, payload, settings) –  POST – The helper checks, if the POST requests contains a payload which is an instance of form data, it converts to toFormData payload.
    • ajax.all(url, payload, settings) – ALL – It helps to deal with concurrent requests.
  • The url is the complete API endpoint, payload consists of the data/request payload. The settings consists of any headers related info, that needs to be added exclusively to the axios config.
  • There is also no need to pass access token to each API request as a payload. The helper check whether the user is logged-in and adds the access_token to the request payload. The snippet below demonstrates it –
if (cookies.get('loggedIn')) {
     payload = {
       access_token: cookies.get('loggedIn'),
       ...payload,
     };
   }
  • The access token, if present in the cookies is added to the payload, therefore authenticating the API call.
  • The keys of the response are changed to camel case before being sent to the caller function. It is done to maintain variable nomenclature standards across the app and also to follow javascript guidelines.
  • The file containing the API calls is structured as follows –
import ajax from '../helpers/ajax';
import urls from '../utils/urls';

const { API_URL } = urls;
const AUTH_API_PREFIX = 'aaa';
const CHAT_API_PREFIX = 'susi';
const CMS_API_PREFIX = 'cms';

// API without request payload
export function fetchApiKeys() {
 const url = `${API_URL}/${AUTH_API_PREFIX}/getApiKeys.json`;
 return ajax.get(url);
}

// API with request payload
export function getLogin(payload) {
 const { email, password } = payload;
 const url = `${API_URL}/${AUTH_API_PREFIX}/login.json`;
 return ajax.get(url, { login: email, password, type: 'access-token' });
}

The above was the implementation of the helper function that is created for making AJAX requests using axios. I hope the blog provided a detailed insight of it helps in making the process of making API calls more standardised and easier.

References

Continue ReadingMake a helper for AJAX requests using axios