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?