Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication between modules #36

Closed
abdurahmanus opened this issue Dec 21, 2018 · 10 comments
Closed

Communication between modules #36

abdurahmanus opened this issue Dec 21, 2018 · 10 comments
Assignees
Labels
How to Issues that ask How to do something in redux-dynamic-modules

Comments

@abdurahmanus
Copy link

What is the preferable way to communicate between modules? Maybe this question is not related directly to this lib. I use redux, redux-saga and redux-dynamic-modules.
The state of my rootModule holds magicBoolean variable. If it becomes true, RootComponent renders SomeLazyComponent, which loads dynamically (I use React.lazy). SomeLazyComponent contains DymanicModuleLoader which loads someDymanicModule. At one point I need to hide SomeLazyComponent and unload someDymanicModule. What I'm trying to do is to fire some action from someDymanicModule (or rootModule) and handle it in reducer (or saga) of rootModule to set magicBoolean variable to false and hide SomeLazyComponent. But in this case I have to import rootActionCreator to someDymanicModule to fire it from there, or to import ROOT_ACTION_NAME to root saga (beacause I need to know action name to handle it). What should I do in this case?

@navneet-g navneet-g pinned this issue Dec 21, 2018
@navneet-g
Copy link
Contributor

navneet-g commented Dec 21, 2018

Communication between modules

Really good question @abdurahmanus, we have come across following patterns for communication among various modules, before I answer the specific details let me list out two specific areas where the modules needs to interact and how we implement in our code.

1) Reading state from other modules

Let us take an example, assume we have three modules

  • CommonModule - which has some shared state used by multiple modules
  • ModuleA - Depends on CommonModule
  • ModuleB - Also depends on CommonModule

We define the dependency as follows using modules.

// CommonModule.js
// -------------------------
function getCommonModule() {
   return {
        id: "common-module",
        reducerMap: {
            commonKey: commonReducer
        }
   }
}

/** ModuleA.js
--------------------------
ModuleA depends on CommonModule, we define the relationship where common module appears in the returned array before moduleA. This ensures that the CommonModule is loaded before ModuleA.
*/
function getModuleA() {
    return [
        getCommonModule(),
        {
            id: "module-a",
            reducerMap: {
                moduleAKey: moduleAReducer
            }
        }
    ]
}


/* ModuleB.js
--------------------------
ModuleB also depends on CommonModule, we define the relationship where common module appears in the returned array before moduleB, this ensures that the CommonModule is loaded before ModuleB redux-dynamic-modules does ref counting so it is ok to load the same module multiple times
*/
function getModuleB() {
    return [
        getCommonModule(),
        {
            id: "module-b",
            reducerMap: {
                moduleBKey: moduleBReducer
            }
        }
    ]
}

Let us now assume we have a component that loads ModuleB, and by that can use state from both CommonModule and ModuleB. We use TypeScript to define interfaces to enforce what part of the state each component can use depending upon what modules they load.

By convention ModuleA should not read stateKeys for CommonModule. CommonModule should define set of selectors that act as an API boundary for other modules to read data owned by CommonModule.

2) Initiating changes in state across modules

As we know any changes to state are in response to Actions. We define shared actions that we call 'SyncActions' these actions can be listened outside module bounderies.

The SyncActions.js file can be used by more than one module in their Reducer.

Note: In Redux all reducers get all Actions, so theoretically we don't need Sync actions, but in a large application this could cause a chaos, we use TypeScript for our application and define separate Actions.js file for each module, those Actions.js file is not imported in any other module. This keeps us disciplined on who can react to which actions.

Finally coming back to your specific question :).
Given the above explanation you can define an action file.

// CommonActions.js
// --------------------------
export const TOGGLE_LAZY_COMPONENT = "CommonActions/TOGGLE_LAZY_COMPONENT";
export function createToggleKayComponent(show: boolean) => { return {
    type: TOGGLE_LAZY_COMPONENT,
    payload: {
        show
    }
}}

Now the saga/component in LazyComponent can dispatch the action, and the reducer in rootComponent can listen to the action and change its state which eventually will lead to Unmounting/Not rendering the LazyComponent in root component.

// RootComponent.js
// --------------------------------------
const RootComponent = ({showLazyComponent:boolean}) => {
    if(showLayComponent) {
        return <LazyComponent />>
    }
    return <div>Hello dear</div>
}

const mapStateToProps = (state) => {
    return {
        showLazyComponent: state.showLazyComponent
    }
}
const ConnectedRootComponent = connect(mapStateToProps)(RootComponent);

@navneet-g navneet-g self-assigned this Dec 21, 2018
@navneet-g navneet-g added the How to Issues that ask How to do something in redux-dynamic-modules label Dec 21, 2018
@abdurahmanus
Copy link
Author

@navneet-g Thanks for the detailed answer! Actually I was also going to ask question 1) Reading state from other modules but you responded before. This approach of defining dependencies looks a bit like injecting modules in angular. And two more questions.

  1. Where do you usually place selectors? Especially selectors which have to read state from multiple modules? For example I need computed data based on commonKey from commonModule and moduleAKey from moduleA somewhere in view which dynamically loads moduleA.
  2. What exactly happens when you define dependencies this way? Common module loads before dependant module? How to get instance of common module from dependant module's files? Importing and using getCommonModule func from CommonModule.js?

@navneet-g
Copy link
Contributor

I have updated the above description to cover selectors.
In short

  1. dynamic-module-loader initializes the modules in the order they are provided.
  2. You can import the file getCommonModule func.
  3. CommonModule should expose some selector functions that ModuleA or ModuleB can use to read data from CommonModule and do calculations with their own data.

@abdurahmanus
Copy link
Author

Thanks a lot! Your description really helped me. It would be great to include these examples in the docs or FAQ. Also it would be very helpful to add small typescript project or some samples because it's not obvious how to structure app, where to put types and interfaces. For example, what type will the root state object have?
type AppState { commonKey: CommonType; moduleAKey?: ModuleAType; moduleBKey?: ModuleBType; }
Should I mark all dymanic keys as optional? And where should I place all these interfaces and types?
If I don't know in advance how many dynamic modules will be in my app, should I describe all these keys and their types in AppState at all.
But as far as I know, when you write mapStateToProps func to get data from state and pass it to component's props you have to deal with the root state obj. So you have to type all it keys in advance. Am I right?
Sorry if I'm not very clear. English is not my native language.

@navneet-g
Copy link
Contributor

This is a really good suggestion and question. To answer these I will write a sample and upload it. Please give me a couple of days.

navneet-g added a commit that referenced this issue Dec 28, 2018
@navneet-g
Copy link
Contributor

@abdurahmanus I have checked in a sample in the above PR, Please look at Typescript Example and let me know if you have questions.

@abdurahmanus
Copy link
Author

@navneet-g Thanks! Typescript Example is clear enough. I still have some questions. Some about this example and some general (design) questions. But I don't know is it ok to discuss these questions here in this issue?

@navneet-g
Copy link
Contributor

Please feel free to ask here.

@abdurahmanus
Copy link
Author

  1. I noticed that in typescript-example (OrderModule.ts) the return type of getOrderModules function is array of modules casted to any. So we lose typings. The type of modules prop of DynamicModuleLoader comp is IModule[]; It always gets array of modules. But sometimes function getXXXs returns array of modules and sometimes getXXX returns individual module. It's a little bit confused especially when return type of getXXX is any.
  2. There is usually a mismatch between shape of data from server and redux store. Where is the right place to put code for api calls, api data interfaces, mappings (normalization). Should it be part of a module too?
  3. Also there is a mismatch between shape of redux store and components props. For instance store holds items in form of a map (object)
    items: { "1": { id: 1, name: "item 1" }, "2": { id: 2, name: "item 2"} }
    But component A needs array:
    items: [ { id: 1, name: "item 1" }, { id: 2, name: "item 2"} ]
    And component B needs something else (maybe computed value based on items obj)
    My question is about selectors. The best practices recomend us to use selectors with mapStateToProps because component should be unaware about the shape of the store. And selectors are usually found with redusers. But in my opinion in order to be reusable modules should be unaware about components and their props. And selectors should not be a part of module. Is that right?

@abettadapur
Copy link
Collaborator

@abdurahmanus

  1. We are still working on the best way to define typings. I think I covered some of it in Typescript Recipes #46
  2. We put our API calls in redux-saga, as it is designed to be the orchestrator layer. Since sagas are part of modules, you can make this logic also be part of your module. As for where you want to transform your data, this is up to the specifics of the application. You could store the raw data in Redux and transform via selector, or transform in the Saga and store in Redux. Your call
  3. Selectors can be part of a module. Components also know which modules they depend on (they must know the data they require). What we do is have the Connected Components import selector functions that live alongside the module contents and these selector functions are written in a generic way that allows the module to decide how it exposes the data it contains. For example, a TodoModule might expose a selector getTodos, which knows how to read the state in the TodoModule. You can also write additional selectors if your needs are more complicated that are more tightly coupled to a component. Our library doesn't prescribe a best practice in this case

Does that make sense?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
How to Issues that ask How to do something in redux-dynamic-modules
Projects
None yet
Development

No branches or pull requests

3 participants