Cross Feature Communication

Most aspects of a feature are internal to the feature's implementation. For example, as a general rule, actions are created and consumed exclusively by logic and reducers that are internal to that feature.

However, there are cases where a feature needs to publicly promote some aspects to another feature. As an example, featureA may:

  • need to know some aspect of featureB (say some state value through a selector),
  • or emit/monitor one of it's actions,
  • or in general anything (i.e. invoke some function that does xyz).

You can think of this as the feature's Public API, and it promotes cross-communication between features.

A best practice is to treat each of your features as isolated implementations. As a result, a feature should never directly import resources from other features, rather they should utilize the public feature promotion of the App object (discussed here). In doing this a: only the public aspects of a feature are exposed/used, and b: your features become truly plug-and-play.

Let's see how Cross Communication is accomplished in feature-u:

publicFace and the App Object

In feature-u, this cross-feature-communication is accomplished through the Feature.publicFace built-in aspect property.

A feature can expose whatever it deems necessary through it's publicFace. There are no real constraints on this resource. It is truly open. Typically it is a container of functions of some sort.

Here is a suggested sampling:

export default createFeature({
  name:     'featureA',

  publicFace: {

    actions: {   // ... JUST action creators that need public promotion (i.e. NOT ALL)
      open: actions.view.open,
    },

    sel: { // ... JUST selectors that need public promotion (i.e. NOT ALL)
      currentView:   selector.currentView,
      isDeviceReady: selector.isDeviceReady,
    },

    api,

  },

  ...
});

The publicFace of all features are accumulated and exposed through the App Object (emitted from launchApp()), as follows:

App.{featureName}.{publicFace}

As an example, the sample above can be referenced like this:

  app.featureA.sel.isDeviceReady(appState)

Accessing the App Object

The App object can be accessed in several different ways.

  1. The simplest way to access the App object is to merely import it.

    Your application mainline exports the launchApp() return value ... which is the App object.

    src/app.js

    ...
    
    // launch our app, exposing the feature-u App object (facilitating cross-feature communication)!
    export default launchApp({
      ...
    });
    

    Importing the app object is a viable technique for run-time functions (such as UI Components), where the code is a: not under the direct control of feature-u, and b: executed after all aspect expansion has completed.

    The following example is a UI Component that displays a deviceStatus obtained from an external startup feature ... accessing the app through an import:

    import app from '~/app';
    
    function ScreenA({deviceStatus}) {
      return (
        <Container>
          ...
          <Text>{deviceStatus}</Text>
          ...
        </Container>
      );
    }
    
    export default connectRedux(ScreenA, {
      mapStateToProps(appState) {
        return {
          deviceStatus: app.device.sel.deviceStatus(appState),
        };
      },
    });
    
  2. Another way to access the App object is through the programmatic APIs of feature-u, where the app object is supplied as a parameter.

    • app life-cycle hooks:

      appWillStart({app, curRootAppElm}): rootAppElm || null
      appDidStart({app, appState, dispatch}): void
      
    • route hooks (PKG: feature-router):

      routeCB({app, appState}): rendered-component (null for none)
      
    • logic hooks (PKG: redux-logic):

      createLogic({
        ...
        transform({getState, action, app}, next) {
          ...
        },
        process({getState, action, app}, dispatch, done) {
          ...
        }
      })
      
  3. There is a third technique to access the App object, that provides early access during code expansion time, that is provided through Managed Code Expansion (see next section).

Managed Code Expansion

In the previous discussion, we detailed two ways to access the App object, and referred to a third technique (discussed here).

There are two situations that make accessing the app object problematic, which are: a: in-line code expansion (where the app may not be fully defined), and b: order dependencies (across features).

To illustrate this, the following redux-logic module is monitoring an action defined by an external feature (see *1*). Because this app reference is made during code expansion time, the import will not work, because the app object has not yet been fully defined. This is a timing issue.

import app from '~/app'; // *1*

export const myLogicModule = createLogic({

  name: 'myLogicModule',
  type: String(app.featureB.actions.fooBar), // *1* app NOT defined during in-line expansion

  process({getState, action}, dispatch, done) {
    ... 
  },

});

When aspect content definitions require the app object at code expansion time, you can wrap the definition in a managedExpansion() function. In other words, your aspect content can either be the actual content itself (ex: a reducer), or a function that returns the content.

Your callback function should conform to the following signature:

API: managedExpansionCB(app): AspectContent

When this is done, feature-u will invoke the managedExpansionCB() in a controlled way, passing the fully resolved app object as a parameter.

To accomplish this, you must wrap your expansion function with the the managedExpansion() utility. The reason for this is that feature-u must be able to distinguish a managedExpansionCB() function from other functions (ex: reducers).

Here is the same example (from above) that that fixes our problem by replacing the app import with managedExpansion():

                             // *1* we replace app import with managedExpansion()
export const myLogicModule = managedExpansion( (app) => createLogic({

  name: 'myLogicModule',
  type: String(app.featureB.actions.fooBar), // *1* app now is fully defined

  process({getState, action}, dispatch, done) {
    ... 
  },

}) );

Because managedExpansionCB() is invoked in a controlled way (by feature-u), the supplied app parameter is guaranteed to be defined (issue a). Not only that, but the supplied app object is guaranteed to have all features publicFace definitions resolved (issue b).

SideBar: A secondary reason managedExpansion() may be used (over and above app injection during code expansion) is to delay code expansion, which can avoid issues related to (legitimate but somewhat obscure) circular dependencies.

App Access Summary

To summarize our discussion of how to access the App object, it is really very simple:

  1. Simply import the app (for run-time functions outside the control of feature-u).

  2. Use the app parameter supplied through feature-u's programmatic APIs (when using route, live-cycle hooks, or logic hooks).

  3. Use the app parameter supplied through managedExpansion() (when app is required during in-line expansion of code).

Accessing Feature Resources in a seamless way is a rudimentary benefit of feature-u that alleviates a number of problems in your code, making your features truly plug-and-play.

NOTE: It is possible that a module may be using more than one of these techniques. As an example a logic module may have to use managedExpansion() to access app at expansion time, but is also supplied app as a parameter in it's functional hook. This is perfectly fine, as they will be referencing the exact same app object instance.

results matching ""

    No results matching ""