0

Importing stores after creating it causes this problem.

store.ts

/**
 * Author: Rahul Shetty
 *
 * The central redux store of our app is created and exported to be used from
 * here.
 */
import { createStore, persist } from 'easy-peasy';
import { services } from '@services/index';
import storage from '@utils/storage';
import { Entities } from 'types/entities';
import storeModel from '@models/index';

// Add any additional store enhancers
let storeEnhancers: any[] = [];

if (__DEV__) {
  const reactotron = require('./reactotron.config').default;
  // @types/ws
  const reactotronConfig = reactotron();

  // Global variable. Use it to log your variable and you can see the result in reactotrons
  (global as any).tronlog = (value: any) => reactotronConfig.log('TRON', value);
  storeEnhancers = [...storeEnhancers, reactotronConfig.createEnhancer()];
}

export const store = createStore(
  persist(storeModel, {
    whitelist: [Entities.LANGUAGES],
    storage,
  }),
  {
    injections: { ...services },
    enhancers: [...storeEnhancers],
  },
); //  create our store

if (__DEV__) {
  // @types/webpack-env
  if (module.hot) {
    // At times the app breaks. Just reload and start again
    module.hot.accept('../models', () => {
      store.reconfigure(storeModel); //  Here is the magic
    });
  }
}

export default store;

The important line to note in the above code snippet is injections: { ...services }. It injects all the services as functions used to make API calls.

services/index.ts

import * as placesServices from './places';
import * as appointmentServices from './appointment';

export const services = {
  placesServices,
  appointmentServices,
};

services/places.ts

import { PlaceServices } from 'types/places';
import { customerAPIInstance } from './api';

export const getPlaces: PlaceServices['getPlaces'] = () =>
  customerAPIInstance.get('/branches');

export const favoritePlace: PlaceServices['favoritePlace'] = (info) =>
  customerAPIInstance.put('/favorite', info);

The below-given code snippet creates a dependency cycle.

services/api.ts

/**
 * Author: Rahul Shetty
 *
 * API Wrapper for the app
 */
import Config from 'react-native-config';
import axios, { Method, AxiosInstance } from 'axios';
import { Entity } from 'types/entities';
import store from '@store/index';
import { ActionCreator } from 'easy-peasy';
import { MetaPayload } from 'types/meta';

type StoreOptions = {
  setPending: ActionCreator<MetaPayload>;
  setError: ActionCreator<MetaPayload>;
  resetError: ActionCreator<MetaPayload>;
};

type APIOptions = {
  method: Method;
  url: string;
  data: DynamicObject;
  entity?: Entity;
};

const { BASE_URL } = Config;

export const customerAPIInstance = axios.create({
  baseURL: BASE_URL,
  timeout: 20000,
  headers: { 'Content-Type': 'application/json' },
});

export const apiConfig = (
  apiInstance: AxiosInstance,
  storeOptions: StoreOptions,
) => async <T>(apiOptions: APIOptions): Promise<T> => {
  const { method, url, data, entity } = apiOptions;
  const { setPending, setError, resetError } = storeOptions;

  /**
   * if the developer doesn't wanna track the asynchronous states, then we avoid
   * calling store actions by not passing the entity name
   */
  if (entity) {
    setPending({
      pending: true,
      entity,
    });
  }

  try {
    const result = await apiInstance({
      method,
      url,
      data,
    });

    // Reset any error if the API call was successful
    if (entity) {
      resetError({
        entity,
      });
    }

    return result.data;
  } catch (err) {
    const message =
      err.response && err.response.data && err.response.data.message
        ? err.response.data.message
        : err.message;

    // Save the error related data if the API call was unsuccessful
    if (entity) {
      setError({
        error: {
          message,
          statusCode: err.status || 500,
        },
        entity,
      });
    }

    throw Error(err);
  } finally {
    // The API call has either successfully resolved or has been rejected.
    // In either case, pending should be set to false
    if (entity) {
      setPending({
        pending: false,
        entity,
      });
    }
  }
};

export const CustomerAPI = apiConfig(customerAPIInstance, {
  setPending: store.getActions().metadata.setPending,
  setError: store.getActions().metadata.setError,
  resetError: store.getActions().metadata.resetError,
});

As you might have observed in the code snippet available above, I am trying to handle error and loading state from a single point using easy-peasy redux actions which are available via the store.

To be specific, import store from '@store/index'; creates the dependency cycle.

But, since injections are nothing but services which in turn uses the store, a dependency cycle is formed.

Store -> injections -> services -> places -> API Instance -> Store

I do have a solution. I could pass the actions from the methods calling the services. For example,

models/places.ts

const placesModel = {
  fetchPlaces: thunk(async(actions, payload, { injections, getStoreActions }) => {
    injections.getPlaces(payload, getStoreActions);
  })
};

But, with the approach shown above, I would have to keep passing the store actions as a second parameter to all the services.

How can I break the dependency cycle by sharing the store actions to make sure the API Instance can set error and loading state from a single location?

shet_tayyy
  • 5,366
  • 11
  • 44
  • 82

1 Answers1

0

The main issue here is importing the store.ts file as it is dependent on services. I made the below-given changes to get rid of this issue.

I created async-handler.ts and added the following code:

import { Entity } from 'types/entities';
import type { Actions, Meta, State, Dispatch } from 'easy-peasy';
import { Services } from 'types/services';
import { StoreModel } from 'types/model';

// @TODO: Simplify the repetitive types
type HandleAsyncStates = <Model extends object = {}, Payload = void>(
  callback: (
    actions: Actions<Model>,
    payload: Payload,
    helpers: {
      dispatch: Dispatch<StoreModel>;
      getState: () => State<Model>;
      getStoreActions: () => Actions<StoreModel>;
      getStoreState: () => State<StoreModel>;
      injections: Services;
      meta: Meta;
    },
  ) => Promise<any>,
) => (
  actions: Actions<Model>,
  payload: Payload,
  helpers: {
    dispatch: Dispatch<StoreModel>;
    getState: () => State<Model>;
    getStoreActions: () => Actions<StoreModel>;
    getStoreState: () => State<StoreModel>;
    injections: Services;
    meta: Meta;
  },
) => Promise<any>;

export const handleAsyncStates: HandleAsyncStates = (callback) => async (
  actions,
  payload,
  helpers,
) => {
  const { getStoreActions, meta } = helpers;
  const { setPending, resetError, setError } = getStoreActions().metadata;

  const entity = meta.path[0] as Entity;

  // Start the pendinng state
  setPending({
    pending: true,
    entity,
  });

  try {
    // Perform you API or async tasks inside the callback, and return the promise
    const result = await callback(actions, payload, helpers);

    // reset any error if it has been set
    resetError(entity);

    return result;
  } catch (err) {
    const message =
      err.response && err.response.data && err.response.data.message
        ? err.response.data.message
        : err.message;

    const errData = {
      message,
      statusCode: err.status || 500,
    };

    // Save the error related data in the store if the API call was unsuccessful
    setError({
      error: errData,
      entity,
    });

    return errData;
  } finally {
    // The API call has either successfully resolved or has been rejected.
    // In either case, pending should be set to false
    setPending({
      pending: false,
      entity,
    });
  }
};

This async handler which acts as a HOC is then passed to the store thunk like so:

  fetchAndSaveNearbyPlaces: thunk(
    handleAsyncStates<PlacesModel, PlaceAPIOption>(
      async (actions, payload, { injections }) => {
        const { placesServices } = injections;
        const response = await placesServices.getPlaces<PlaceListResponse>();

        actions.saveNearbyPlaces(response.data.result);

        return response;
      },
    ),
  ),

The store thunk provides the necessary actions and services required to perform the common store actions such as tracking the API async states and eliminates the need to import store.ts altogether.

shet_tayyy
  • 5,366
  • 11
  • 44
  • 82