diff --git a/README.md b/README.md index 57f1d0a..09eacc0 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ export function getUsersModule(): IModule { * Create a `ModuleStore` ```typescript -import {configureStore, IModuleStore} from "redux-dynamic-modules"; +import {createStore, IModuleStore} from "redux-dynamic-modules"; import {getUsersModule} from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 63b617b..d8e6e62 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -30,10 +30,10 @@ export function getUsersModule(): IModule { * Create a `ModuleStore` ```typescript -import {configureStore, IModuleStore} from "redux-dynamic-modules"; +import {createStore, IModuleStore} from "redux-dynamic-modules"; import {getUsersModule} from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/docs/reference/Extensions.md b/docs/reference/Extensions.md index 523059f..2d4db03 100644 --- a/docs/reference/Extensions.md +++ b/docs/reference/Extensions.md @@ -21,9 +21,9 @@ export interface IExtension { ``` ## Adding extensions to the store -To add an extension to the `ModuleStore`, pass it as the second argument to `configureStore` +To add an extension to the `ModuleStore`, pass it as the second argument to `createStore` ```typescript -const store: IModuleStore = configureStore({}, [getMyExtension()]) +const store: IModuleStore = createStore({}, [getMyExtension()]) ``` diff --git a/docs/reference/ModuleStore.md b/docs/reference/ModuleStore.md index 3036f91..fcbabdd 100644 --- a/docs/reference/ModuleStore.md +++ b/docs/reference/ModuleStore.md @@ -5,14 +5,14 @@ The **Module Store** is a Redux store with the added capability of managing **Re * `addModules(modules: IModule[])`: Same as `addModule`, but for multiple modules. The return function will remove all the added modules. * `dispose()`: Remove all of the modules added to the store and dispose of the object -To create a `ModuleStore`, use the `configureStore` function from our package +To create a `ModuleStore`, use the `createStore` function from our package ## Example {docsify-ignore} ```typescript -import { configureStore, IModuleStore } from "redux-dynamic-modules"; +import { createStore, IModuleStore } from "redux-dynamic-modules"; import {getUsersModule} from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/docs/reference/ReduxObservable.md b/docs/reference/ReduxObservable.md index 4135062..0d2b1d0 100644 --- a/docs/reference/ReduxObservable.md +++ b/docs/reference/ReduxObservable.md @@ -3,14 +3,14 @@ You can use `redux-dynamic-modules` alongside `redux-observable` so that you can To use * `npm install redux-dynamic-modules-observable` -* Add the observable extension to the `configureStore` call +* Add the observable extension to the `createStore` call ```typescript -import { configureStore, IModuleStore } from "redux-dynamic-modules"; +import { createStore, IModuleStore } from "redux-dynamic-modules"; import { getObservableExtension } from "redux-dynamic-modules-observable"; import { getUsersModule } from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/docs/reference/ReduxSaga.md b/docs/reference/ReduxSaga.md index 4472772..6de4e21 100644 --- a/docs/reference/ReduxSaga.md +++ b/docs/reference/ReduxSaga.md @@ -3,14 +3,14 @@ You can use `redux-dynamic-modules` alongside `redux-saga` so that you can add/r To use * `npm install redux-dynamic-modules-saga` -* Add the saga extension to the `configureStore` call +* Add the saga extension to the `createStore` call ```typescript -import { configureStore, IModuleStore } from "redux-dynamic-modules"; +import { createStore, IModuleStore } from "redux-dynamic-modules"; import { getSagaExtension } from "redux-dynamic-modules-saga"; import { getUsersModule } from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/docs/reference/ReduxThunk.md b/docs/reference/ReduxThunk.md index ad0c38e..51aad3a 100644 --- a/docs/reference/ReduxThunk.md +++ b/docs/reference/ReduxThunk.md @@ -3,14 +3,14 @@ You can use `redux-dynamic-modules` alongside `redux-thunk`. To use * `npm install redux-dynamic-modules-thunk` -* Add the thunk extension to the `configureStore` call +* Add the thunk extension to the `createStore` call ```typescript -import { configureStore, IModuleStore } from "redux-dynamic-modules"; +import { createStore, IModuleStore } from "redux-dynamic-modules"; import { getThunkExtension } from "redux-dynamic-modules-thunk"; import { getUsersModule } from "./usersModule"; -const store: IModuleStore = configureStore( +const store: IModuleStore = createStore( /* initial state */ {}, diff --git a/packages/redux-dynamic-modules-saga/src/__tests__/SagaExtension.test.ts b/packages/redux-dynamic-modules-saga/src/__tests__/SagaExtension.test.ts index a8feed7..aef789e 100644 --- a/packages/redux-dynamic-modules-saga/src/__tests__/SagaExtension.test.ts +++ b/packages/redux-dynamic-modules-saga/src/__tests__/SagaExtension.test.ts @@ -1,4 +1,4 @@ -import { configureStore } from "redux-dynamic-modules"; +import { createStore } from "redux-dynamic-modules"; import { getSagaExtension } from "../SagaExtension"; import { ISagaModule } from "../Contracts"; import { SagaIterator } from "redux-saga"; @@ -6,7 +6,7 @@ describe("Saga extension tests", () => { it("Saga extension registers module and starts saga", () => { const testContext = {}; called = false; - configureStore({}, [getSagaExtension(testContext)], getTestModule()); + createStore({}, [getSagaExtension(testContext)], getTestModule()); expect(called); expect(testContext["moduleManager"]).toBeTruthy(); diff --git a/packages/redux-dynamic-modules/src/Contracts.ts b/packages/redux-dynamic-modules/src/Contracts.ts index b1b3e71..7546ba3 100644 --- a/packages/redux-dynamic-modules/src/Contracts.ts +++ b/packages/redux-dynamic-modules/src/Contracts.ts @@ -51,6 +51,9 @@ export interface IModuleManager { * Add the given module to the store */ addModule: (module: IModule) => IDynamicallyAddedModule + /** + * Adds the given set of modules to the store + */ addModules: (modules: IModule[]) => IDynamicallyAddedModule; } diff --git a/packages/redux-dynamic-modules/src/Managers/ModuleManager.ts b/packages/redux-dynamic-modules/src/Managers/ModuleManager.ts index 3ad853a..921316e 100644 --- a/packages/redux-dynamic-modules/src/Managers/ModuleManager.ts +++ b/packages/redux-dynamic-modules/src/Managers/ModuleManager.ts @@ -60,7 +60,7 @@ export function getModuleManager(middlewareManager: IItemManager { if (_reducerManager) { return _reducerManager.reduce(s, a); diff --git a/packages/redux-dynamic-modules/src/Managers/ReducerManager.ts b/packages/redux-dynamic-modules/src/Managers/ReducerManager.ts index e0f3a53..2640868 100644 --- a/packages/redux-dynamic-modules/src/Managers/ReducerManager.ts +++ b/packages/redux-dynamic-modules/src/Managers/ReducerManager.ts @@ -63,6 +63,11 @@ export function getReducerManager( } keysToRemove = []; } + + if (state === undefined) { + state = {} as S; + } + return combinedReducer(state, action); }; diff --git a/packages/redux-dynamic-modules/src/ModuleEnhancer.ts b/packages/redux-dynamic-modules/src/ModuleEnhancer.ts new file mode 100644 index 0000000..f07d560 --- /dev/null +++ b/packages/redux-dynamic-modules/src/ModuleEnhancer.ts @@ -0,0 +1,129 @@ +import { + applyMiddleware, + DeepPartial, + StoreEnhancer, + StoreCreator, + Reducer, + Action, + compose as composeEnhancers +} from "redux"; +import { IModule, IExtension, IModuleStore } from "./Contracts"; +import { getModuleManager } from "./Managers/ModuleManager"; +import { getRefCountedManager } from "./Managers/RefCountedManager"; +import { getMiddlewareManager } from './Managers/MiddlewareManager'; + +/** + * Adds dynamic module management capabilities to a redux store. + * @param extensions: Optional. Any extensions for the store e.g. to support redux-saga or redux-thunk + * @param initialModules: Optional. Any modules to bootstrap the store with + */ +export function moduleEnhancer( + extensions: IExtension[] = [], + initialModules: IModule[] = []): StoreEnhancer, S1> { + + return (createStore: StoreCreator) => + ( + baseReducer?: Reducer, + preloadedState?: DeepPartial, + baseEnhancer?: StoreEnhancer) => { + + // get middlewares from extensions if any + const extensionMiddlewares = extensions.reduce( + (mw, p) => { + if (p.middleware) { + mw.push(...p.middleware) + } + return mw; + }, + [] + ); + + // create manager to manage dynamic middlewares + const middlewareManager = getRefCountedManager( + getMiddlewareManager(), + (a, b) => a === b); + + // create module manager + const moduleManager = getRefCountedManager( + getModuleManager( + middlewareManager, + extensions), + (a: IModule, b: IModule) => a.id === b.id); + + // Create module enhancer to manage extensions and dynamic middlewares + const moduleEnhancer = composeEnhancers( + applyMiddleware(...extensionMiddlewares, + middlewareManager.enhancer)); + + // compose the moduleEnahancer with the enhancers passed, the order matters as we want to be additive + // so the right parameter is the enhancer we received + const composedEnhancer = + (baseEnhancer ? + composeEnhancers(moduleEnhancer, baseEnhancer) : + moduleEnhancer); + + // build a chained reducer + const chainedReducer = (state, action) => { + // call the passed in reducer first + const intermediateState = baseReducer ? baseReducer(state, action) : state; + // then delegate to the module managers reducer + return moduleManager.getReducer(intermediateState, action); + }; + + // Create the store + const store = createStore(chainedReducer, preloadedState, composedEnhancer); + + // module manager will use the dispatch from store to dispatch initial and final actions + moduleManager.setDispatch(store.dispatch); + + // adds given modules to mouldManager + const addModules = (modulesToBeAdded: IModule[]) => { + moduleManager.add(modulesToBeAdded); + return { + remove: () => { + moduleManager.remove(modulesToBeAdded); + } + }; + } + + // Adds the module to the module manager + const addModule = (moduleToBeAdded: IModule) => { + return addModules([moduleToBeAdded]); + }; + + + const dispose = () => { + // get all added modules and remove them + moduleManager.dispose(); + middlewareManager.dispose(); + extensions.forEach(p => { + if (p.dispose) { + p.dispose(); + } + }); + }; + + // Allow extensions to react when modules are added + extensions.forEach(p => { + if (p.onModuleManagerCreated) { + p.onModuleManagerCreated({ + addModule, + addModules + }); + } + }); + + const moduleStore = { + addModule, + addModules, + dispose + } + + // Add the initial modules + moduleStore.addModules(initialModules); + return { + ...store as any, + ...moduleStore + }; + } +} \ No newline at end of file diff --git a/packages/redux-dynamic-modules/src/ModuleStore.ts b/packages/redux-dynamic-modules/src/ModuleStore.ts index f0f1199..501e9a1 100644 --- a/packages/redux-dynamic-modules/src/ModuleStore.ts +++ b/packages/redux-dynamic-modules/src/ModuleStore.ts @@ -1,95 +1,23 @@ -import { applyMiddleware, compose, createStore, DeepPartial } from "redux"; +import { createStore as reduxCreateStore, DeepPartial } from "redux"; import { IModule, IModuleStore, IExtension } from "./Contracts"; -import { getModuleManager } from "./Managers/ModuleManager"; -import { getRefCountedManager } from "./Managers/RefCountedManager"; -import { getMiddlewareManager } from './Managers/MiddlewareManager'; +import { moduleEnhancer } from './ModuleEnhancer'; /** * Configure the module store */ -export function configureStore(initialState: DeepPartial, extensions: IExtension[], reduxModule: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule, m7: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule, m7: IModule, m8: IModule): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], ...initialModules: IModule[]): IModuleStore; -export function configureStore(initialState: DeepPartial, extensions: IExtension[], ...initialModules: IModule[]): IModuleStore { - if (!extensions) { - extensions = []; - } - - const extensionMiddleware = extensions.reduce( - (mw, p) => { - if (p.middleware) { - mw.push(...p.middleware) - } - - return mw; - }, - [] - ); - - let composeEnhancers = compose; - - //@ts-ignore - if (process.env.NODE_ENV === "development") { - composeEnhancers = window["__REDUX_DEVTOOLS_EXTENSION_COMPOSE__"] || compose; - } - - const middlewareManager = getRefCountedManager(getMiddlewareManager(), (a, b) => a === b); - const enhancer = composeEnhancers(applyMiddleware(...extensionMiddleware, middlewareManager.enhancer)); - const modules = getRefCountedManager(getModuleManager(middlewareManager, extensions), (a: IModule, b: IModule) => a.id === b.id); - - // Create store - const store: IModuleStore = createStore( - modules.getReducer, - initialState, - enhancer - ) as IModuleStore; - - modules.setDispatch(store.dispatch); - - const addModules = (modulesToBeAdded: IModule[]) => { - modules.add(modulesToBeAdded); - return { - remove: () => { - modules.remove(modulesToBeAdded); - } - }; - } - - const addModule = (moduleToBeAdded: IModule) => { - return addModules([moduleToBeAdded]); - }; - - extensions.forEach(p => { - if (p.onModuleManagerCreated) { - p.onModuleManagerCreated({ - addModule, - addModules - }); - } - }); - - store.addModule = addModule; - store.addModules = addModules; - - store.dispose = () => { - // get all added modules and remove them - modules.dispose(); - middlewareManager.dispose(); - extensions.forEach(p => { - if (p.dispose) { - p.dispose(); - } - }); - }; - - - store.addModules(initialModules); - - return store as IModuleStore; -} +export function createStore(initialState: DeepPartial, extensions: IExtension[], reduxModule: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule, m7: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], m1: IModule, m2: IModule, m3: IModule, m4: IModule, m5: IModule, m6: IModule, m7: IModule, m8: IModule): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], ...initialModules: IModule[]): IModuleStore; +export function createStore(initialState: DeepPartial, extensions: IExtension[], ...initialModules: IModule[]): IModuleStore { + return reduxCreateStore( + /* reducer */undefined, + initialState, + moduleEnhancer(extensions, initialModules) + ); +} \ No newline at end of file diff --git a/packages/redux-dynamic-modules/src/__tests__/Extensions.test.ts b/packages/redux-dynamic-modules/src/__tests__/Extensions.test.ts index 6338110..2858d7c 100644 --- a/packages/redux-dynamic-modules/src/__tests__/Extensions.test.ts +++ b/packages/redux-dynamic-modules/src/__tests__/Extensions.test.ts @@ -1,5 +1,5 @@ import { IExtension, IModuleStore } from "../Contracts"; -import { configureStore } from "../ModuleStore"; +import { createStore } from "../ModuleStore"; import { Middleware } from "redux"; describe("Store with extensions", () => { @@ -22,25 +22,25 @@ describe("Store with extensions", () => { middlewareFunction(); }; testExtension.middleware = [middleware]; - testStore = configureStore({}, [testExtension]); + testStore = createStore({}, [testExtension]); testStore.dispatch({ type: "ANY" }); expect(middlewareFunction).toHaveBeenCalled(); }); it("Manager created called", () => { - testStore = configureStore({}, [testExtension]); + testStore = createStore({}, [testExtension]); expect(testExtension.onModuleManagerCreated).toHaveBeenCalled(); }); it("OnModule Added called", () => { - testStore = configureStore({}, [testExtension]); + testStore = createStore({}, [testExtension]); testStore.addModule({ id: "new_module" }); expect(testExtension.onModuleAdded).toHaveBeenCalled(); }); it("OnModule Removed called", () => { - testStore = configureStore({}, [testExtension]); + testStore = createStore({}, [testExtension]); const module = testStore.addModule({ id: "new_module" }); module.remove(); diff --git a/packages/redux-dynamic-modules/src/__tests__/ModuleEnhancer.test.ts b/packages/redux-dynamic-modules/src/__tests__/ModuleEnhancer.test.ts new file mode 100644 index 0000000..5c5c8c1 --- /dev/null +++ b/packages/redux-dynamic-modules/src/__tests__/ModuleEnhancer.test.ts @@ -0,0 +1,36 @@ +import { moduleEnhancer } from '../ModuleEnhancer'; +import { + createStore, + compose, + applyMiddleware +} from 'redux'; + +it("Validate with applyMiddleware enhancer", () => { + const store = createStore( + testReducer, + compose( + moduleEnhancer(), + applyMiddleware(testMiddleware) + )); + + // validate that the store has addModule + expect(!!store.addModule).toBe(true); + + // validate that the base enhancer is working + store.dispatch({ type: "hello world" }); +}); + + +const testReducer = (state, action) => { + if (action.type.indexOf("@") === -1) { + expect(action.enhanced).toBe("foo"); + } + return (state || 1) + 1; +} + +const testMiddleware = () => { + return (next) => action => { + action.enhanced = "foo"; + return next(action); + } +} \ No newline at end of file diff --git a/packages/todo-example/src/index.js b/packages/todo-example/src/index.js index 40c090a..1c6a0c7 100644 --- a/packages/todo-example/src/index.js +++ b/packages/todo-example/src/index.js @@ -1,10 +1,10 @@ import React from 'react' import { render } from 'react-dom' -import { configureStore } from 'redux-dynamic-modules' +import { createStore } from 'redux-dynamic-modules' import { Provider } from 'react-redux' import App from './components/App' -const store = configureStore({}, []); +const store = createStore({}, []); render( diff --git a/packages/widgets-example/src/App.js b/packages/widgets-example/src/App.js index be162d7..8eac5f4 100644 --- a/packages/widgets-example/src/App.js +++ b/packages/widgets-example/src/App.js @@ -2,8 +2,8 @@ import React, { Component } from 'react'; import { Provider } from "react-redux"; // We will load the widgets async using react-loadable. import Loadable from "react-loadable"; -// configureStore allows us to load/unload modules dynamically. -import { configureStore } from "redux-dynamic-modules"; +// createStore allows us to load/unload modules dynamically. +import { createStore } from "redux-dynamic-modules"; // Saga extension allows us to use Saga middleware in the module store. import { getSagaExtension } from "redux-dynamic-modules-saga"; // Thunk extension allows us to use Thunk middleware in the module store. @@ -25,7 +25,7 @@ class App extends Component { * The extensions are optional and you can choose extension based on the middleware you use * You can also build your own extensions for any other middleware e.g. redux-observable */ - this.store = configureStore({}, [getThunkExtension(), getSagaExtension()]); + this.store = createStore({}, [getThunkExtension(), getSagaExtension()]); } render() {