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.

  1. Built-In aspects:

    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.

  2. Extendable aspects:

    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, or redux-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).

  • Feature.appWillStart()

    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).

  • Feature.appInit()

    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).

  • Feature.appDidStart()

    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 to redux!

    It configures redux through the reducerAspect (to be supplied to launchApp()), which extends the Feature object, adding support for the Feature.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 by slicedReducer(), 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.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 to launchApp()), which extends the Feature object, adding support for the Feature.route property, referencing routes defined through the featureRoute() 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 ReactDOM     from 'react-dom';
import {launchApp}  from 'feature-u';
import features     from 'features';
import aspects      from 'aspects';
import {splash}     from 'util/SplashScreen';

// launch our app, exposing the Fassets object (facilitating cross-feature-communication)
export default launchApp({         // *4*

  features,                        // *1*
  aspects,                         // *2*

  registerRootAppElm(rootAppElm) { // *3*
    ReactDOM.render(rootAppElm,
                    document.getElementById('root'));
  },

  showStatus(msg='', err=null) {   // *5*
    splash(msg, err);
  },
});
  • All app features are supplied (accumulated from the features/ directory) ... see: *1* (above) and Feature Accumulation

  • The app aspects (i.e. the run-time stack) are supplied (accumulated from the aspects/ directory) ... see: *2* (above) and Aspect Accumulation

  • 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 (like Feature.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:

  1. Any logic that runs after the return of launchApp() should not assume that the app is "up and running". Rather you should rely on the Feature.appDidStart() Application Life Cycle Hook.

    In reality, this has always been an implicit "best practice".

  2. Any errors that occur in the Feature.appInit() life cycle methods (and beyond) are reported via that showStatus() 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.

results matching ""

    No results matching ""