This article is an introduction to a new JS library called feature-u, that facilitates feature-based development in your React project.

Note: On 8/14/2018 feature-u V1 was released, that re-designed Cross Feature Communication to include UI Composition as a core offering. This article covers the V1 release. The first article, based on feature-u V0, can be found here. We are very excited about this update because it promotes one solution for all feature collaboration!

Most developers would agree that organizing your project by feature is much preferred over type-based patterns. Because application domains grow in the real world, project organization by type simply doesn’t scale, it just becomes unmanageable!

There are a number of good articles that discuss this topic with insight on feature-based design and structure (see: References below). However when it comes to the implementation, you are pretty much left to fend for yourself.

feature-u is a utility library that manages and streamlines this process. It automates the mundane details of managing features and helps to promote features that are truly plug-and-play.

This article provides a foundation of feature-u concepts and terminology, building insight into how you can promote individual plug-and-play features within your project. It makes the case for why feature-u was developed and gives you a better understanding of it’s benefits.

Check out the full docs, source, and npm package.

feature-u opens new doors into the exciting world of feature-based development. It frees you up to focus your attention on the “business end” of your features!

At a Glance

For your convenience, this Table of Contents (TOC) links directly to each section. Also note that each section title links back to the TOC.

Feature Based Development  Segregating Features  Feature Goals    Feature Runtime Consolidation    Feature CollaborationThe feature-u Solution  launchApp()  Feature Object  aspects  Running the App    App Initialization    Framework Configuration    Launching Your Application  Cross Feature Communication  Feature Based UI Composition    Resource Contracts  Feature EnablementIn SummaryBenefitsReferences
Please help me get the word out on feature-u. Your claps determine the distribution/promotion of this article. If you think feature-u has potential, please give this article multiple claps :-)

Feature Based Development

At a 30,000 ft view, feature-based development (as in most software) is all about dissecting hard problems into smaller pieces. Even when I started my career (back in the 70's), this was a prominent quote:

“All problems in computer science can be solved by another level of indirection.” David Wheeler

By breaking up your application into features, each feature can focus on a more specific and isolated set of tasks. In some ways you can think of a feature as a “mini application”!

There are many design considerations in defining your feature boundaries. You can find several articles on this topic that provide insight on feature-based design.

For the most part, these considerations are part of the design of each individual project. While feature-u does not dictate overall design considerations, it does facilitate good feature-based principles (such as encapsulation). This will be the focus of this article.

Segregating Features

If you are like me, when you think about feature-based development, the first thing that comes to mind is to isolate your code into feature directories.

In doing this your code is organized by what it accomplishes (i.e. features), rather than what it is (i.e. components, routes, logic, actions, reducers, selectors, etc.).

By segregating your features into individual directories, there is a semblance of isolation.

Feature Goals

Our goal is to encapsulate each feature in such a way as to make them truly plug-and-play. But how is this accomplished?

The directory structure is just a start. There are several hurdles that must be overcome to realize our goal …

  • How do we encapsulate and isolate our features, while still allowing them to collaborate with one another?
  • How can selected features introduce start-up initialization (even injecting utility at the root DOM), without relying on some external startup process?
  • How can feature-based UI Composition be accomplished in an isolated and autonomous way?
  • How do we configure our chosen frameworks now that our code is so spread out?
  • How do we enable/disable selected features which are either optional, or require a license upgrade?

In short, how do we achieve a running application from these isolated features?

When you boil it all down, there are two overriding characteristics that must be accomplished to achieve our goals:

  1. Feature Runtime Consolidation: pulling our features back together into one running application
  2. Feature Collaboration: provide a mechanism by which our features can interact with one another

As it turns out, everything else is a byproduct of these two artifacts. Let’s take a closer look at each of these items.

Feature Runtime Consolidation

Now that we have isolated our features into separate entities, how do we bring them back together so they run as one application? We must be able to pull and configure various aspects of our individual features, and “launch” them as a single homogeneous running application.

This concern can be further divided into two sub-concerns:

  • App Initialization
    Some features may require certain startup initialization. As an example, a feature that encapsulates some DB abstraction will rely on a run-time setup of a DB service.
    Certainly we don’t want to rely on some global app logic to accomplish this (once again, we want our features to be encapsulated and self-sufficient).
  • Framework Configuration
    If your application relies on other frameworks, chances are there are resources contained within each feature that must be accumulated and fed into the framework configuration process.
    How is this accomplished?

Feature Collaboration

The second characteristic (mentioned above) is Feature Collaborationproviding a mechanism by which our features can interact with one another.

A best practice of feature-based development (to the extent possible) is to treat each feature as an isolated implementation. Most aspects of a feature are internal to that feature’s implementation (for example, actions are typically created and consumed exclusively by logic/reducers/components that are internal to that feature).

From this perspective, you can think of each feature as its own isolated mini application.

With that said, however, we know that no man is an island! Any given feature ultimately exists as part of a larger application. There are cases where a feature needs to promote a limited subset of its aspects to other features. For example, a feature may need to:

  • be knowledgeable of some external state (via a selector)
  • emit or monitor actions of other features
  • consolidate component resources from other features — as in UI Composition
  • invoke the API of other features
  • etc. etc. etc.

These items form the basis of why Cross Feature Communication and Feature Based UI Composition are needed.

To complicate matters, as a general rule, JS imports should NOT cross feature boundaries. The reason being that this cross-communication should be limited to public access points — helping to facilitate true plug-and-play.

Given all this then, how is Cross Feature Communication achieved in a way that doesn’t break encapsulation?

Features need a way to promote their Public Interface to other features, and consume other feature’s Public Assets.

The feature-u Solution

Let’s take a look at the solution feature-u provides for all of these goals. The following sections will build feature-u concepts incrementally.

launchApp()

launchApp() is an essential utility in feature-u. It is an agent, working on your behalf, which provides the foundation that accomplishes all the goals of feature-u! It facilitates both Feature Runtime Consolidation and Feature Collaboration.

With this utility, your mainline startup process is extremely simple … it merely invokes launchApp(), and you are done!

The launchApp() function actually starts your application running, employing various hooks that drive BOTH App Initialization and Framework Configuration!

You can find launchApp() examples in the Usage section, and Launching Your Application.

How does this work? What are the bindings to launchApp()? ... let's delve a bit deeper…

Feature Object

To accomplish this, each feature promotes a Feature object (using createFeature()), that catalogs aspects of interest to feature-u.

This is the primary input to launchApp().

aspects

In feature-u, “aspect” (little “a”) is a generalized term used to refer to the various ingredients that (when combined) constitute your application. Aspects can take on many different forms: UI ComponentsRoutesState Management(actions, reducers, selectors)Business LogicStartup Initialization Codeetc. etc. etc.

Not all aspects are of interest to feature-uonly those that are needed to setup and launch the application … all others are considered 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.

The Feature object is merely a lightweight container that holds aspects of interest to feature-u. These aspects can either be Built-In aspects (from core feature-u), or Extendable aspects (from plugin extensions).

Running the App

Let’s see how launchApp() accommodates the two sub-goals of running the app:

App Initialization

Because launchApp() is in control of starting the app, it can introduce Application Life Cycle Hooks.

This allows each feature to perform app-specific initialization, and even inject components into the root of the app.

There are two hooks:

  1. Feature.appWillStart() - invoked one time at app startup time
  2. Feature.appDidStart() - invoked one time immediately after app has started

Application Life Cycle Hooks greatly simplify your app's mainline startup process, because initialization specific to a given feature can be encapsulated in that feature.

Framework Configuration

A fundamental goal of feature-u is to automatically configure the framework(s) used in your run-time-stack (by accumulating the necessary resources across all your features). This greatly reduces the boilerplate code within your app.

How can this be accomplished when there are so many frameworks out there … and every project uses a different mix?

feature-u is extendable! It operates in an open plugable architecture where Extendable Aspects integrate feature-u to other frameworks, matching your specific run-time stack. This is good, because not everyone uses the same frameworks!

Extendable Aspects can be found in external NPM packages (the normal case), or you can create your own using createAspect() (a more advanced topic).

The Aspect object contains a series of Aspect Life Cycle Hooks that are invoked under the control of feature-u(launchApp()). In general, an Aspect's responsibility is to:

  • accumulate AspectContent across all features
  • perform some desired setup and configuration
  • expose it’s functionality in some way (typically a framework integration)

An Aspect automatically extends the Feature object by allowing it's AspectContent to be "cataloged" in the Feature using Aspect.name as it's key. In the diagram above, you can see that

  • the reducerAspect (Aspect.name: 'reducer') permits a Feature.reducer: reducerContent construct
  • and the logicAspect (Aspect.name: 'logic') permits a Feature.logic: logicContent construct

It is important to understand that the interface to your chosen frameworks is not altered in any way. You use them the same way you always have (just within your feature boundary). feature-u merely provides a well-defined organizational layer, where the frameworks are automatically setup and configured by accumulating the necessary resources across all your features.

Launching Your Application

In feature-u, the 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 Aspects and Features, and starts the app by invoking launchApp():

Here are some important points of interest (match the numbers to *n* in the code above):

  1. the supplied Aspects (pulled from separate npm packages) reflect the frameworks of our run-time stack (in our example redux, redux-logic, and feature-router) and extend the acceptable Feature properties (Feature.reducer, Feature.logic, and Feature.route respectively) ... see: Extendable aspects
  2. all of our app features are supplied (accumulated from the features/ directory)
  3. a registerRootAppElm() callback is used to catalog the supplied rootAppElm to the specific React platform in use. Because this registration is accomplished by your app-specific code, feature-u can operate in any of the React platforms, such as: react-web, react-native, and expo ... see: React Registration
  4. as a bit of a preview, the return value of launchApp() is a Fassets object, which promotes the accumulated Public Face of all features, and is exported to provide Cross Feature Communication.

Cross Feature Communication

In support of Feature Collaboration that doesn’t break encapsulation, feature-u promotes feature-based resources through something called fassets (feature assets). This is how all Cross Feature Communication is accomplished. You can think of this as the Public Face of a feature.

SideBar: The term fassets is a play on words. While it is pronounced "facet" and is loosely related to this term, it is spelled fassets (i.e. feature assets).

A feature can expose whatever it deems necessary through the built-in Feature.fassets aspect). There is no real constraint on this resource. It is truly open.

The fassets aspect has a define directive where resources are cataloged.

Here is a simple example of how fassets are defined:

feature-u accumulates fassets from all active features, and promotes them through the Fassets object (emitted from launchApp()).

SideBar: There are several ways to obtain access the Fassets object (see Obtaining fassets object).

To reference a fassets resource, simply dereference it as any other object reference. There is also a Fassets.get()method that can be supplied Wildcards, returning an array of resources.

This is an example of a push philosophy. Here the supplier is is simply publicly promoting a resource for other features to use (take it or leave it). The supplier is merely saying: “this is my Public Face”.

You can find more information about this topic in Cross Feature Communication.

Feature Based UI Composition

It is common for a UI component to be an accumulation of sub-components that span several features. As a result, UI Composition is a very important part of Cross Feature Communication.

In support of this, feature-u introduces the withFassets() Higher-order Component (HoC) that auto-wires fasset properties into a component. This is a common pattern popularized by Redux connect() (simplifying component access to application state).

Here is how a component would access a company.logo (defined by another feature):

The withFassets() HoC auto-wires named feature assets as component properties through the mapFassetsToPropsStruct hook. In this example, because the Logo property is a component, MyComponent can simply reference it using JSX.

You can find more information about this topic in UI Composition.

Resource Contracts

It is common for UI Composition to be represented as a contract, where a component in one feature has a series of injection needs that are to be supplied by other features.

The fassets aspect has additional constructs to facilitate this contractual arrangement, allowing feature-u to provide more validation in the process.

Rather than just defining resources in one feature and using them in another:

  • A given feature can specify a series of injection needs using the fassets.use directive. This identifies a set of injection keys that uniquely identify these resources.
  • Other features will supply this content using the fassets.defineUse directive, by referencing these same injection keys.

This represents more of a pull philosophy. It gives feature-u more knowledge of the process, allowing it to verify that supplied resources are correct.

Wildcards (*) can be used to add additional dynamics to the process, allowing features to inject their content autonomously.

Here is a main feature that is pulling in a series of sub-components (links and bodies) from other features:

main feature:

Because our specification includes wildcards, a series of definitions will match!

Here is the MainPage component that fulfills the usage contract:

When withFassets() encounters wildcards (*), it merely accumulates all matching definitions, and promotes them as arrays.

Through this implementation, any feature may dynamically inject itself in the process autonomously! In addition, this dynamic implicitly handles the case where a feature is dynamically disabled (very kool indeed)!!

The following snippets are taken from other features that supply the definitions for the content to inject:

cart feature

search feature

Two external features (cart and search) define the content that is requested by the main feature.

The fassets.defineUse directive requires that the resource keys match a fassets.use feature request. This is the contract that provides feature-u insight when enforcing it's validation.

SideBar: Because we are also dealing with navigation, we introduce react-router into the mix (with the Link and Route components). Because of RR's V4 design, our routes are also handled through component composition (see Feature Based Routes for more information).

You can find more information about this topic in UI Composition.

Feature Enablement

Features can be dynamically disabled by setting the Feature.enabled boolean property (part of the Built-In aspects):

In this example, it is just as though the sandbox feature doesn't exist. In other words it has been logically removed.

Typically, this indicator is based on some run-time expression, allowing packaged code to be dynamically enabled/disabled during the application’s start-up process:

This dynamic is useful in a number of different situations. For example:

  • some features may require a license upgrade
  • other features may only be used for diagnostic purposes, and are disabled by default

You can find more information about this topic in Feature Enablement.

In Summary

The following diagram summarizes feature-u’s Basic Concepts (as discussed above):

Benefits

There are many benefits in using feature-u!

The two fundamental artifacts from which most benefits are derived are:

  • A formal means by which features can collaborate with one another (Cross Feature Communication), making them truly plug-and-play
    This includes the ability for UI Composition to cross feature boundaries. It even allows UI Content to be injected autonomously. This is something that has to be seen ... it shows off feature-u very well.
  • A significant reduction in boilerplate code through:
    Auto configuration of the frameworks in-use (via plugin extensions — Extendable aspects)
    Startup initialization that is encapsulated within features (via Application Life Cycle Hooks)

The following list of benefits can be directly correlated to the considerations that formed the basis of why feature-u was developed (see: Why feature-u?).

  1. Feature Encapsulation: isolating feature boundaries improves code manageability
  2. Feature Collaboration: promote Cross Feature Communication through a well-defined feature-based Public Interface
  3. Feature Based UI Composition: facilitate seamless cross-feature component composition
  4. Application Life Cycle Hooks: features can initialize themselves without relying on an external process
  5. Feature Enablement: enable/disable features through a run-time switch
  6. Minimize Feature Order Dependency Issues during in-line code expansion
  7. Framework Integration: automatically configure used framework(s) (matching the app’s run-time-stack) by accumulating all feature aspects (employing an extendable API)
  8. UI Component Promotion: features can autonomously promote their UI components through Feature Based Route Management
  9. Single Source of Truth: is facilitated in a number of ways within a feature’s implementation
  10. Simplified App Startup: launching an app can be accomplished through a single line of executable code!
  11. Operates in any React Platform React Web, React Native, Expo, etc.
  12. Plug-and-Play: features can be more easily added or removed

feature-u allows you to focus your attention on the “business end” of your features!

Go forth and compute!!

References