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.
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 externalstartup
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), }; }, });
Another way to access the
App
object is through the programmatic APIs of feature-u, where theapp
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) { ... } })
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:
Simply import the app (for run-time functions outside the control of feature-u).
Use the app parameter supplied through feature-u's programmatic APIs (when using route, live-cycle hooks, or logic hooks).
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.