application/index.js

/* 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;