Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock api in react redux-thunk project returning undefined

I am quite new in redux world and have not yet had a project structured the ducks way. I am trying to understand it and use it to make a mock api, since I don't have the backend ready yet. I am working with the legacy code, that I am trying to figure out. There is a folder called data, that has a duck and a backendApi file. Duck file looks like this.

data/duck.jsx

import { createSelector } from 'reselect';
import { createReduxApi } from './backendApi';

const getDataContext = state => state.default.dataContext;

const backendReduxApi = createBackendReduxApi(getDataContext);

// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);

export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);

// Reducers
export const dataReducer = backendReduxApi .createReducer();

// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);

The backendApi.jsx file looks like this:

data/backendApi.jsx

import ReduxRestApi from './rest/ReduxRestApi';

export const BackendApi = { // NOSONAR
  LANGUAGE_FILE: 'languageFile',
  EMPLOYEE: 'employee',
};

const backendReduxApiBuilder = ReduxRestApi.build()
  /* /api */

  /* /api/employee */
  .withGet('/myproject/api/employee', BackendApi.EMPLOYEE)

  /* /language*/
  .withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)

export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
  .withRestApiSelector(restApiSelector)
  .create();

Then in the data/rest folder I have 4 files: ReduxRestApi, restConfig, RestDuck and restMethods.

data/rest/ReduxRestApi.jsx

import { combineReducers } from 'redux';
import { get, post, postAndOpenBlob } from './restMethods';
import RestDuck from './RestDuck';

class ReduxRestApi {
  constructor(endpoints, getRestApiState) {
    this.createReducer = this.createReducer.bind(this);
    this.getEndpoint = this.getEndpoint.bind(this);
    this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
    this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
    this.getEndpointState = this.getEndpointState.bind(this);
    this.ducks = endpoints.map(({ name, path, restMethod }) => new RestDuck(name, path, restMethod, getRestApiState));
  }

  createReducer() {
    const reducers = this.ducks
      .map(duck => ({ [duck.name]: duck.reducer }))
      .reduce((a, b) => ({ ...a, ...b }), {});
    return combineReducers(reducers);
  }

  getEndpoint(endpointName) {
    return this.ducks.find(duck => duck.name === endpointName)
      || { actionCreators: {} };
  }

  makeRequestActionCreator(endpointName) {
    return this.getEndpoint(endpointName).actionCreators.execRequest;
  }

  makeResetActionCreator(endpointName) {
    return this.getEndpoint(endpointName).actionCreators.reset;
  }

  getEndpointState(endpointName) {
    return this.getEndpoint(endpointName).stateSelector;
  }

  static build() {
    class RestApiBuilder {
      constructor() {
        this.withGet = this.withGet.bind(this);
        this.withPost = this.withPost.bind(this);
        this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
        this.withRestApiSelector = this.withRestApiSelector.bind(this);
        this.endpoints = [];
      }

      withGet(path, name) {
        this.endpoints.push({ path, name, restMethod: get });
        return this;
      }

      withPost(path, name) {
        this.endpoints.push({ path, name, restMethod: post });
        return this;
      }

      withPostAndOpenBlob(path, name) {
        this.endpoints.push({ path, name, restMethod: postAndOpenBlob });
        return this;
      }

      withRestApiSelector(restApiSelector) {
        this.restApiSelector = restApiSelector;
        return this;
      }

      create() {
        return new ReduxRestApi(
          this.endpoints,
          this.restApiSelector
        );
      }
    }

    return new RestApiBuilder();
  }
}

export default ReduxRestApi;

restConfig.jsx

import axios from 'axios';
import { removeErrorMessage, showErrorMessage } from '../../app/duck';
import { is401Error, isHandledError } from '../../app/ErrorTypes';

const isDevelopment = process.env.NODE_ENV === 'development';

const configureRequestInterceptors = (store) => {
  const onRequestAccepted = (config) => {
    store.dispatch(removeErrorMessage());
    return config;
  };

  const onRequestRejected = error => Promise.reject(error);

  axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
};

const configureResponseInterceptors = (store) => {
  const onSuccessResponse = response => response;

  const onErrorResponse = (error) => {
    if (is401Error(error) && !isDevelopment) {
      window.location.reload();
    }
    if (!isHandledError(error)) {
      store.dispatch(showErrorMessage(error));
    }
    return Promise.reject(error);
  };

  axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
};

const configureRestInterceptors = (store) => {
  configureRequestInterceptors(store);
  configureResponseInterceptors(store);
};

export default configureRestInterceptors;

data/rest/RestDuck.jsx

import { createSelector } from 'reselect';

import { get, getBlob, post, postAndOpenBlob, postBlob } from './restMethods';

/**
 * getMethodName
 * Helper function that maps given AJAX-method to a name
 *
 * Ex. getMethodName(getBlob) -> 'GET'
 */
const getMethodName = (restMethod) => {
  switch (restMethod) {
    case get:
    case getBlob:
      return 'GET';
    case post:
    case postBlob:
    case postAndOpenBlob:
      return 'POST';
    default:
      return '';
  }
};

/**
 * createRequestActionType
 * Helper function to generate actionType for actions related to AJAX calls
 *
 * Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '@@REST/fetchEmployee GET /myproject/api/employeeERROR'
 */
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`@@REST/${name}`, getMethodName(restMethod), path, qualifier]
  .filter(s => s !== '')
  .join(' ');

/**
 * createRequestActionTypes
 * Helper function to generate ActionTypes for a given AJAX method and resource.
 *
 * Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> {
 *   reset: '@@REST GET /myproject/api/employee RESET',
 *   requestStarted: '@@REST GET /myproject/api/employee STARTED',
 *   requestError: '@@REST GET /myproject/api/employee ERROR',
 *   requestFinished: '@@REST GET /myproject/api/employee FINISHED',
 * }
 */
const createRequestActionTypes = (name, restMethod, path) => ({
  reset: createRequestActionType(name, 'RESET'),
  requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
  requestError: createRequestActionType(name, 'ERROR', restMethod, path),
  requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
});

/**
 * createRequestThunk
 * Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
 *
 * When the thunk is running, the action 'requestStarted' will be dispatched immediately.
 * Then, it performs the AJAX call that returns a promise.
 *  If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
 * If the call fails, the action 'requestError' is dispatched with the contents of the error.
 */
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
  (params, options = {}) => (dispatch) => {
    dispatch(requestStarted(params, options));
    return restMethod(restEndpoint, params)
      .catch((error) => {
        const data = error.response && error.response.data ? error.response.data : error;
        dispatch(requestError(data));
        return Promise.reject(error);
      })
      .then((response) => {
        dispatch(requestFinished(response.data));
        return response;
      });
  }
);

/**
 * createRequestActionCreators
 * Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
 * @see createRequestThunkCreator
 */
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => {
  const reset = () => ({ type: actionTypes.reset });
  const requestStarted = (params, options = {}) => ({ type: actionTypes.requestStarted, payload: { params, timestamp: Date.now() }, meta: { options } });
  const requestFinished = data => ({ type: actionTypes.requestFinished, payload: data });
  const requestError = error => ({ type: actionTypes.requestError, payload: error });
  const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
  return {
    reset, requestStarted, requestFinished, requestError, execRequest
  };
};

/**
 * createRequestReducer
 *
 * Helper function that creates a reducer for an AJAX call.
 * Reducer alters the state of the actions with the name defined by
 *   actionTypes.requestStarted
 *   actionTypes.requestFinished
 *   actionTypes.requestError
 */
const createRequestReducer = (restMethod, resourceName, actionTypes) => {
  const initialState = {
    data: undefined,
    meta: undefined,
    error: undefined,
    started: false,
    finished: false
  };

  return (state = initialState, action = {}) => {
    switch (action.type) {
      case actionTypes.requestStarted:
        return {
          ...initialState,
          data: action.meta.options.keepData ? state.data : initialState.data,
          started: true,
          meta: action.payload
        };
      case actionTypes.requestFinished:
        return {
          ...state,
          started: false,
          finished: true,
          data: action.payload
        };
      case actionTypes.requestError:
        return {
          ...state,
          started: false,
          error: action.payload
        };
      case actionTypes.reset:
        return {
          ...initialState
        };
      default:
        return state;
    }
  };
};

/**
 * RestDuck
 * Class that offers action types, action creators, reducers and selectors for an AJAX call.
 * @see createRequestActionTypes
 * @see createRequestActionCreators
 * @see createRequestReducer
 *
 * Ex.
 * const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
 * // Action creators
 * export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
 * // Reducer
 * export const dataReducer = combineReducers(
 *   ...,
 *   getEmployeeDuck.reducer,
 * }
 * // Selectors
 * export const getDataContext = state => state.default.dataContext;
 * export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
 * export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
 * ...
 */
class RestDuck {
  constructor(name, path, restMethod, getApiContext) {
    this.restMethod = restMethod;
    this.name = name;
    this.path = path;
    this.getApiContext = getApiContext;
    this.$$duck = {}; // for class internal use
  }

  get actionTypes() {
    if (!this.$$duck.actionTypes) {
      this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
    }
    return this.$$duck.actionTypes;
  }

  get actionCreators() {
    if (!this.$$duck.actionCreators) {
      this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
    }
    return this.$$duck.actionCreators;
  }

  get reducer() {
    if (!this.$$duck.reducer) {
      this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
    }
    return this.$$duck.reducer;
  }

  get stateSelector() {
    return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
  }
}

export default RestDuck;

data/rest/restMethods.jsx

import axios, { CancelToken } from 'axios';

const openPreview = (data) => {
  if (window.navigator.msSaveOrOpenBlob) {
    window.navigator.msSaveOrOpenBlob(data);
  } else {
    window.open(URL.createObjectURL(data));
  }
};

const cancellable = (config) => {
  let cancel;
  const request = axios({
    ...config,
    cancelToken: new CancelToken((c) => { cancel = c; })
  });
  request.cancel = cancel;
  return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
};

const defaultHeaders = {
  'Cache-Control': 'no-cache',
  Pragma: 'no-cache',
  Expires: 0
};

const defaultPostHeaders = {
  'Content-Type': 'application/json'
};

export const get = (url, params, responseType = 'json') => cancellable({
  url,
  params,
  responseType,
  method: 'get',
  headers: {
    ...defaultHeaders
  }
});

export const post = (url, data, responseType = 'json') => cancellable({
  url,
  responseType,
  data: JSON.stringify(data),
  method: 'post',
  headers: {
    ...defaultHeaders,
    ...defaultPostHeaders
  },
  cache: false
});

export const getBlob = (url, params) => get(url, params, 'blob');

export const postBlob = (url, data) => post(url, data, 'blob');

export const postAndOpenBlob = (url, data) => postBlob(url, data)
  .then((response) => {
    openPreview(response.data);
    return {
      ...response,
      data: 'blob opened as preview' // Don't waste memory by storing blob in state
    };
  });

I am not sure where to place and how to do mock api calls in this structure. I was thinking of making a mock api similiar to this one, where I would mimick the ajax calls and store them in the redux, but just not sure how to do this in this kind of setup?

I have tried with making the mockApi folder and instead of using the restMethods, to use the file where I would write promises that would resolve the mockData. This is my attempt:

mockRestMethods

const employee = {
  name: 'Joe Doe'
}
const data = {
  employee 
};

export const get = item => new Promise((resolve) => {
  setTimeout(() => {
    resolve({ data: data[item] });
  }, 1000);
});

But, if I inspect what is returned as the response.data inside the createRequestThunk function in the RestDuck file I get data: undefined there. Why is that, what am I doing wrong?

like image 916
Leff Avatar asked Aug 30 '18 08:08

Leff


2 Answers

I may well have this wrong, but it seems like you are replacing

export const get = (url, params, responseType = 'json') => cancellable({

with export const get = item => new Promise((resolve) => { which has a different API.

Regardless, have you tried logging the value of item in the mock get function. I'm guessing it isn't "employee" which is the only property in data.


Yes, that was my goal, to replace the call that was pointing to the backend API, with the call where I would return the mock data. I have tried to log the value of the item, but I get undefined

ok, so there's an awful lot of abstraction going on there. Id suggest starting by replacing get in data/rest/restMethods.jsx directly with a version that returns a promise, get it working, and then break it out. That way you're not dealing with too many unknowns at once.

like image 85
lecstor Avatar answered Sep 18 '22 22:09

lecstor


I had done similar using redux-saga. After debugging, I had found that there must be data property as root key. Here's how you should do:

const employee = {
  data: { // root key of employee
    items: [
      { name: 'Bhojendra' },
      { name: 'Rauniyar' }
    ]
  }
}

// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))

Now, I hope you know why data is undefined with your code.

Still not clear?

Response looks for the data property. That's it.

like image 30
Bhojendra Rauniyar Avatar answered Sep 19 '22 22:09

Bhojendra Rauniyar