/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { PersistGate } from 'redux-persist/integration/react';
import { persistReducer, persistStore } from 'redux-persist';
// removed until redux-injectors support react-redux 8.1.1
// import { createInjectorsEnhancer, forceReducerReload } from 'redux-injectors';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/rootSaga';
import reducers, { setAppInfoAction } from '../state';
import { Provider } from 'react-redux';
const APP_DATA = {
name: '',
hash: false,
thunk: false,
reducers: { ...reducers },
sagas: {},
rootReactComponent: null,
persistSetup: null,
reduxSagaMonitorOptions: {},
store: null,
persistor: null,
};
const addReducers = externalReducers => {
if (externalReducers) {
// check if reducer with existing key is added
Object.keys(externalReducers).forEach(key => {
if (APP_DATA.reducers[key] !== undefined) {
throw new Error(
`You are trying to add a reducer with key '${key}' which already exists. To avoid losing functionality, please, change the key name.`
);
}
});
}
APP_DATA.reducers = {
...APP_DATA.reducers,
...externalReducers,
};
};
const rootReducer = () =>
combineReducers({
...APP_DATA.reducers,
});
const createReducer = () =>
APP_DATA.persistSetup !== null ? persistReducer(APP_DATA.persistSetup, rootReducer()) : rootReducer();
export const injectReducer = (key, asyncReducer) => {
if (!APP_DATA.reducers[key]) {
APP_DATA.reducers[key] = asyncReducer;
APP_DATA.store.replaceReducer(createReducer());
APP_DATA.persistor.persist();
}
};
/**
* @param runSaga runSaga is middleware.run function
* @returns a function that can be used to inject sagas dynamically
*/
function createSagaInjector(runSaga) {
// Create a dictionary to keep track of injected sagas
const injectedSagas = new Map();
const isInjected = key => injectedSagas.has(key);
const injectSaga = (key, saga) => {
// We won't run saga if it is already injected
if (isInjected(key)) return;
// Sagas return task when they executed, which can be used to cancel them
const task = runSaga(saga);
// Save the task if we want to cancel it in the future
injectedSagas.set(key, task);
};
// Inject the root saga as it a statically loaded file,
injectSaga('root', rootSaga);
return injectSaga;
}
const injectSagas = sagas => {
if (sagas) {
Object.keys(sagas).forEach(key => {
APP_DATA.store.injectSaga(key, sagas[key]);
});
}
};
export const injectSaga = (key, saga) => {
APP_DATA.store.injectSaga(key, saga);
APP_DATA.sagas[key] = saga;
APP_DATA.persistor.persist();
};
const getMiddlewares = defaultMiddleware =>
defaultMiddleware({
thunk: APP_DATA.thunk,
serializableCheck: false,
});
const createStore = () => {
const sagaMiddleware = createSagaMiddleware(APP_DATA.reduxSagaMonitorOptions);
const allMiddlewares = [sagaMiddleware];
const { run: runSaga } = sagaMiddleware;
const enhancers = [];
// removed until redux-injectors support react-redux 8.1.1
// const enhancers = [
// createInjectorsEnhancer({
// createReducer,
// runSaga,
// }),
// ];
APP_DATA.store = configureStore({
reducer: createReducer(),
middleware: getDefaultMiddleware => getMiddlewares(getDefaultMiddleware).concat(...allMiddlewares),
enhancers: getDefaultEnhancers => getDefaultEnhancers().concat(enhancers),
// eslint-disable-next-line no-undef
devTools: !JSON.parse(IS_PRODUCTION), // defined in webpack
});
APP_DATA.persistor = persistStore(APP_DATA.store, () => {
APP_DATA.store.getState();
});
APP_DATA.store.injectSaga = createSagaInjector(runSaga);
injectSagas(APP_DATA.sagas);
// removed until redux-injectors support react-redux 8.1.1
// if (module?.hot) {
// module.hot?.accept('./reducers', () => {
// forceReducerReload(APP_DATA.store);
// });
// }
APP_DATA.store.dispatch(setAppInfoAction({ appInfo: { appName: APP_DATA.name } }));
};
const renderApplication = () => {
const Router = APP_DATA.hash ? HashRouter : BrowserRouter;
const container = APP_DATA.rootDomElement;
const root = createRoot(container);
APP_DATA.persistSetup !== null
? root.render(
<Provider store={APP_DATA.store}>
<PersistGate persistor={APP_DATA.persistor}>
<Router>{APP_DATA.rootReactComponent}</Router>
</PersistGate>
</Provider>
)
: root.render(
<Provider store={APP_DATA.store}>
<Router>{APP_DATA.rootReactComponent}</Router>
</Provider>
);
};
const init = () => {
createStore();
renderApplication();
};
const setupAppData = data => {
if (data.name !== undefined) {
APP_DATA.name = data.name;
}
if (data.getRootDomElement !== undefined) {
APP_DATA.rootDomElement = data.getRootDomElement();
}
if (data.rootReactComponent !== undefined) {
APP_DATA.rootReactComponent = data.rootReactComponent;
} else {
throw new Error('`rootReactComponent` is required');
}
if (data.hash !== undefined) {
APP_DATA.hash = data.hash;
}
if (data.persistSetup !== undefined) {
APP_DATA.persistSetup = data.persistSetup;
}
};
export const createOlafApplication = data => {
setupAppData(data);
if (data.projectVersion !== undefined) {
process.env.PROJECT_VERSION = data.projectVersion || '1.0.0';
}
addReducers(data.reducers);
APP_DATA.sagas = data.sagas;
init();
return { store: APP_DATA.store, persistor: APP_DATA.persistor };
};
/**
* Main application component
*
* @param {any} props
*/
function Application(props) {
useEffect(() => {
createOlafApplication(props);
}, []);
}
export default Application;