A Closer Look
As previously mentioned, the basic process of feature-u is that
each feature promotes a Feature
object that catalogs
various aspects of that feature ... things like: the feature's name,
it's Public Face, whether it is enabled, initialization constructs, and
resources used to configure it's slice of the frameworks in use.
In turn, these Feature objects are supplied to launchApp()
,
which configures and starts your application, returning an
Fassets object
(which promotes the Public Face of each
feature).
Let's take a closer look at this process ...
aspects
In feature-u, "aspect" is a generalized term used to refer to the various ingredients that (when combined) constitute your application. Aspects can take on many different forms ... for example:
- UI Components and Routes,
- State Management (actions, reducers, selectors),
- Business Logic,
- Startup Initialization Code,
- etc.
Not all aspects are of interest to feature-u ... only those that are needed to setup and launch the app ... all others are considered to be an internal implementation detail of the feature. As an example, consider the redux state manager: while it uses actions, reducers, and selectors ... only reducers are needed to setup and configure redux.
feature-u provides a base set of built-in aspects (out-of-the-box) but allows additional aspects to be introduced through it's extendable API.
-
These aspects are promoted directly by the base feature-u package. They provide very rudimentary capabilities, such as feature enablement, Public Face, and application life-cycle hooks.
-
These aspects are promoted by external packages (or self defined in your project). They provide feature-u integration with other frameworks (for example
redux
state management, orredux-logic
business logic, or navigational routers, etc.). They are created with feature-u's extendable API, and are packaged separately, so as to not introduce unwanted dependencies.
Feature Object (relaying aspect content)
The Feature
object is merely a lightweight container that
holds AspectContent
of interest to feature-u.
Each feature within an application promotes a Feature object (using
createFeature()
) which catalogs the aspects of that feature.
Ultimately, all Feature objects are consumed by launchApp()
.
Feature content are simple key/value pairs (the key being an Aspect.name with values of AspectContent). These aspects can either be built-in (from core feature-u), or extensions.
Here is an example:
export default createFeature({
name: 'featureA', // builtin aspect (name must be unique across all features within app)
enabled: true, // builtin aspect enabling/disabling feature
fassets: { // builtin aspect promoting Public Face - Cross Feature Communication
define: {
'api.openA': () => ...,
'api.closeA': () => ...,
},
},
appWillStart: (...) => ..., // builtin aspect (Application Life Cycle Hook)
appInit: (...) => ..., // ditto
appDidStart: (...) => ..., // ditto
reducer: ..., // feature redux reducer (extended aspect from the feature-redux plugin)
logic: ..., // feature logic modules (extended aspect from the feature-redux-logic plugin)
});
Built-In aspects
Built-in aspects are promoted directly by the base feature-u package. They provide very rudimentary capabilities, such as feature enablement, Public Face, and application life-cycle hooks.
Like all aspects, Built-In aspect content is relayed through Feature
object properties (via createFeature()
).
Feature.name
A string property which represents the identity of the feature. Feature names are guaranteed to be unique. Application code can also use
Feature Name
in various single-source-of-truth operations.
Feature.enabled
A boolean property that determines whether the feature is enabled or not. This indicator is typically based on a dynamic expression, allowing packaged code to be dynamically enabled/disabled at run-time (please refer to:
Feature Enablement
).
-
An optional aspect that promotes feature assets used in
Cross Feature Communication
(i.e. the Public Face of a feature).fassets
directives can both define resources, and/or declare a resource contract (the intention to use a set of fasset resources). Resources are accumulated across all features, and exposed through theFassets object
, along with theuseFassets()
Hook andwithFassets()
HoC. For more information, please seeFeature.fassets aspect
.
-
An optional
Application Life Cycle Hook
invoked one time, just before the app starts up. This life-cycle hook can do any type of initialization, and/or optionally supplement the app's top-level content (using a non-null return) (please refer to:appWillStart
). -
An optional
Application Life Cycle Hook
invoked one time, later in the app startup process. This life-cycle hook supports blocking async initialization (by simply returning a promise) (please refer to:appInit
). -
An optional
Application Life Cycle Hook
invoked one time, immediately after the app has started. Because the app is up-and-running at this time, you have access to the appState and the dispatch() function ... assuming you are using redux (when detected by feature-u's plugable aspects) (please refer to:appDidStart
).
Extendable aspects
feature-u is extendable! Extendable Aspects provide feature-u
integration with other frameworks (for example redux
state management, or redux-logic
business logic, etc.).
For this reason (by in large) they provide the most value,
because they fully integrate your features into your run-time
stack!
Extendable Aspects are packaged separately from feature-u, so as to not introduce unwanted dependencies (because not everyone uses the same frameworks). You pick and choose them based on the framework(s) used in your project (matching your project's run-time stack).
Extendable Aspects are created with feature-u's
Extension API
, using createAspect()
. You can
define your own Aspect (if the one you need doesn't already exist)!
For more information, please see Extending feature-u
.
Like all aspects, Extendable Aspect content is relayed through Feature
object properties (via createFeature()
).
Because Extendable Aspects are not part of the base feature-u
package, it is a bit problematic to discuss them here (they are
either in a separate npm package, or self contained in your project).
You should search the npm registry with the 'feature-u'
keyword
to find the ones that meet your requirements. With that said, we
will briefly discuss the Extendable Aspects that were created in
conjunction with the initial development of feature-u (just to give
you a feel of what is possible).
Feature.reducer
via:feature-redux
feature-redux
is the feature-u integration point toredux
!It configures
redux
through thereducerAspect
(to be supplied tolaunchApp()
), which extends the Feature object, adding support for theFeature.reducer
property, referencing feature-based reducers.Only reducers are of interest because that is all that is needed to configure [redux]. All other redux items (actions, selectors, etc.) are considered to be an internal implementation detail of the feature.
The
Feature.reducer
content must be embellished byslicedReducer()
, which provides instructions on how to combine multiple feature-based reducers in constructing the overall top-level application state tree.Because feature-redux manages redux, it also provides an integration hook to other Aspects that need to inject redux middleware.
Please refer to the
feature-redux
documentation for complete details.
Feature.logic
via:feature-redux-logic
feature-redux-logic
is the feature-u integration point toredux-logic
!It configures
redux-logic
through thelogicAspect
(to be supplied tolaunchApp()
), which extends the Feature object, adding support for theFeature.logic
property, referencing feature-based logic modules.The following article is an introduction (and motivation) for the development of redux-logic: Where do I put my business logic in a React-Redux application.
Please refer to the
feature-redux-logic
documentation for complete details.
Feature.route
via:feature-router
feature-router
is the feature-u integration point to Feature Routes!It configures Feature Router through the
routeAspect
(to be supplied tolaunchApp()
), which extends the Feature object, adding support for theFeature.route
property, referencing routes defined through thefeatureRoute()
function.Feature Routes are based on a very simple concept: allow the application state to drive the routes! It operates through a series of registered functional callback hooks, which determine the active screen based on an analysis of the the overall appState. This is particularly useful in feature-based routing, because each feature can promote their own UI components in an encapsulated and autonomous way! Because of this, feature-router is a preferred routing solution for feature-u.
Feature Routes is the recommended solution for feature-based navigation. You can find more information in the
Feature Based Routes
section.Please refer to the
feature-router
documentation for complete details.
Launching Your Application
By interpreting the set of Features and Aspects that comprise an application, feature-u can actually coordinate the launch of your application (i.e. start it running)!
This is accomplished through the launchApp()
function.
It manages the setup and configuration of the frameworks in use, such as
redux
,redux-logic
, etc. This is based on a set of supplied plugable Aspects that extend feature-u, integrating external frameworks to match your specific run-time stack.It facilitates application life-cycle hooks on the
Feature
object, allowing features to manage things like: initialization and injecting root UI elements, etc.It creates and promotes the
Fassets object
which contains the Public Face of all features, facilitating a cross-communication between features.
As a result, your application mainline is very simple and generic.
There is no real app-specific code in it ... not even any global
initialization! That is because each feature can inject their own
app-specific constructs!! The mainline merely accumulates the
Features and Aspects, and starts the app by invoking launchApp()
:
src/app.js
import React from 'react';
import ReactDOM from 'react-dom';
import {launchApp} from 'feature-u';
import features from './features'; // *2*
import {createReducerAspect} from 'feature-redux'; // *1*
import {createLogicAspect} from 'feature-redux-logic'; // *1*
import {createRouteAspect} from 'feature-router'; // *1*
import SplashScreen {splash} from './util/comp/SplashScreen';
// launch our app, exposing the feature-u Fassets object (facilitating cross-feature-communication)!
export default launchApp({ // *4*
features, // *2*
aspects: appAspects(), // *1*
registerRootAppElm(rootAppElm) { // *3*
ReactDOM.render(rootAppElm,
getElementById('myAppRoot'));
},
showStatus(msg='', err=null) { // *5*
splash(msg, err);
},
});
// accumulate/configure the Aspect plugins matching our app's run-time stack
function appAspects() {
// define our framework run-time stack
const reducerAspect = createReducerAspect(); // *1*
const logicAspect = createLogicAspect(); // *1*
const routeAspect = createRouteAspect(); // *1*
const aspects = [ // *1*
reducerAspect, // redux ... extending: Feature.reducer
logicAspect, // redux-logic ... extending: Feature.logic
routeAspect, // Feature Routes ... extending: Feature.route
];
// configure Aspects (as needed)
// ... StateRouter fallback screen (when no routes are in effect)
routeAspect.config.fallbackElm$ = <SplashScreen msg="I'm trying to think but it hurts!"/>;
// beam me up Scotty :-)
return aspects;
}
All of our supplied app features are accumulated from the
features/
directory ... (see*2*
in the code snippet above).The Aspect collection (see
*1*
in the code snippet above) reflects the frameworks of our run-time stack (in our exampleredux
,redux-logic
, andfeature-router
) and extend the acceptable Feature properties (Feature.reducer
,Feature.logic
, andFeature.route
respectively) ... see:Extendable aspects
. In this case, all our Aspects were pulled from external npm packages, however you can define your own usingcreateAspect()
.The
showStatus()
callback parameter (see*5*
in the code snippet above) uses a SplashScreen to communicate status messages to the end user, resulting from long-running async processes (likeFeature.appInit()
).
Export fassets
The Fassets object
(emitted from launchApp()
)
promotes the accumulated Public Face of all features. This is the
basis of Cross Feature Communication
.
By feature-u decree, this object should be exported (see *4*
in the code snippet above), providing import access to other modules.
SideBar: In reality there are several ways to access the
fassets
object (depending on the context), import is just one
(see: Obtaining fassets object
).
You can find more information about the Fassets object
in
Cross Feature Communication
.
React Registration
The launchApp()
function uses a
registerRootAppElm()
callback (see *3*
in the code
snippet above) to catalog the supplied rootAppElm
to the specific
React platform in use.
API: registerRootAppElm(rootAppElm, fassets): void
NOTE regarding the rootAppElm
:
Typically the supplied
rootAppElm
will have definition, based on the Features and Aspects that are in use. In this case, it is the responsibility of this callback to register this content in some way (either directly or indirectly).However, there are atypical isolated cases where the supplied
rootAppElm
can be null. This can happen when the app chooses NOT to use Features/Aspects that inject any UI content. In this case, the callback is free to register it's own content.
Because this registration is accomplished by your app-specific code, feature-u can operate in any of the React platforms, such as:
React Web
import ReactDOM from 'react-dom';
...
export default launchApp({
features,
aspects,
registerRootAppElm(rootAppElm) { // *3*
ReactDOM.render(rootAppElm,
getElementById('myAppRoot'));
}
});
React Native
import {AppRegistry} from 'react-native';
...
export default launchApp({
features,
aspects,
registerRootAppElm(rootAppElm) { // *3*
AppRegistry.registerComponent('myAppKey',
()=>rootAppElm); // convert rootAppElm to a React Component
}
});
Expo
import Expo from 'expo';
...
export default launchApp({
features,
aspects,
registerRootAppElm(rootAppElm) { // *3*
Expo.registerRootComponent(()=>rootAppElm); // convert rootAppElm to a React Component
}
});
Covert Asynchronicity
It is worth noting that "under the covers" launchApp()
is
actually an asynchronous function, in a "covert" sort of way.
For simplicity (and backward compatibility), launchApp()
returns just before any
Feature.appInit()
/ Feature.appDidStart()
Application Life Cycle Hooks
execute. The returned
Fassets object
is fully resolved and functional.
However, launchApp()
is still potentially working in the
background, due to the blocking nature of the asynchronous
Feature.appInit()
Application Life Cycle Hook
(introduced in
V2.1.0
).
This heuristic has two ramifications:
Any logic that runs after the return of
launchApp()
should not assume that the app is "up and running". Rather you should rely on theFeature.appDidStart()
Application Life Cycle Hook
.In reality, this has always been an implicit "best practice".
Any errors that occur in the
Feature.appInit()
life cycle methods (and beyond) are reported via thatshowStatus()
callback.
The bottom line of launchApp()
is that it's "Covert
Asynchronous" behavior is "by design", because it seems reasonable
(in the authors opinion), and has the added benefit of being backward
compatible to all prior feature-u versions.