Feature Based Navigation (using redux state)
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src
tooling
.babelrc
.eslintrc
.gitignore
.travis.yml
CHANGELOG.md
LICENSE.md
README.md
SCRIPTS.md
package-lock.json
package.json
webpack.config.babel.js

README.md

feature-router - Feature Based Navigation (using redux state)

feature-router is your feature-u integration point to Feature Routes! It promotes the routeAspect (a feature-u plugin) that integrates Feature Routes into your features.

Backdrop:

    feature-u is a utility that facilitates feature-based project organization for your react project. It helps organize your project by individual features. feature-u is extendable. It operates under an open plugin architecture where Aspects integrate feature-u to other framework/utilities that match your specific run-time stack.

Build Status Codacy Badge Codacy Badge Known Vulnerabilities NPM Version Badge

Overview:

    feature-router configures Feature Routes through the routeAspect (which is supplied to feature-u's launchApp()). This extends feature-u's Feature object by adding support for the Feature.route property, referencing the routeCB() hook specified through the featureRoute() function.

    Feature Routes is based on a very simple concept: allow the redux 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.

At a Glance

Install

  • peerDependencies ... you should already have these, because this is our integration point (but just in case):

    npm install --save feature-u
    npm install --save react
    npm install --save redux
    npm install --save react-redux
  • the main event:

    npm install --save feature-router

SideBar: Depending on how current your target browser is (i.e. it's JavaScript engine), you may need to polyfill your app (please refer to Potential Need for Polyfills).

Usage

  1. Within your mainline, register the feature-router routeAspect (see: **1**) to feature-u's launchApp().

    Please note that routeAspect has a required config.fallbackElm$ configuration item (see: **2**).

    Also note that redux must be present in your run-time stack, because the routes ultimately analyze state managed by redux (see: **3**).

    src/app.js

    import {launchApp}            from 'feature-u';
    import {createRouteAspect}    from 'feature-router'; // **1**
    import {createReducerAspect}  from 'feature-redux';  // **3**
    import SplashScreen           from '~/util/comp/SplashScreen';
    import features               from './feature';
    
    // configure Aspects (as needed) // **2**
    const routeAspect = createRouteAspect();
    routeAspect.config.fallbackElm$ = <SplashScreen msg="I'm trying to think but it hurts!"/>;
    
    export default launchApp({
    
      aspects: [
        routeAspect,                 // **1**
        createReducerAspect(),       // **3**
        ... other Aspects here
      ],
    
      features,
    
      registerRootAppElm(rootAppElm) {
        ReactDOM.render(rootAppElm,
                        getElementById('myAppRoot'));
      }
    });
  2. Within each feature that promotes UI Screens, simply register the feature's route through the Feature.route property (using feature-u's createFeature()).

    Here is a route for a startup feature that simply promotes a SplashScreen until the system is ready. It's route references a routeCB() (see **4**) defined through the featureRoute() function (see **5**):

    Note that this example has a HIGH route priority, giving it precedence over other routes at a lower priority (see: **6**).

    src/feature/startup/index.js

    import React            from 'react';
    import {createFeature}  from 'feature-u';
    import {featureRoute, 
            PRIORITY}       from 'feature-router';
    import * as selector    from './state';
    import SplashScreen     from '~/util/comp/SplashScreen';
    
    export default createFeature({
    
      name:  'startup',
    
      route: featureRoute({              // **5** 
        priority: PRIORITY.HIGH,         // **6**
        content({fassets, appState}) {   // **4**
          if (!selector.isDeviceReady(appState)) {
            return <SplashScreen msg={selector.getDeviceStatusMsg(appState)}/>;
          }
          return null;  // system IS ready ... allow downstream routes to activate
        },
      }),
    
      ... snip snip
    });

    The Feature.route property can either reference a single featureRoute() or multiple (an array) with varying priorities.

This should give you the basic idea of how Feature Routes operate. The following sections develop a more thorough understanding of Feature Route concepts. Go forth and compute!

A Closer Look

You may be surprised to discover that feature-u recommends it's own flavor of route management. There are so many! Why introduce yet another?

As it turns out, feature-u does not dictate any one navigation/router solution. You are free to use whatever route/navigation solution that meets your requirements.

  • You can use the recommended Feature Routes (i.e. this package)
  • You can use XYZ navigation (fill in the blank with your chosen solution)
  • You can even use a combination of Feature Routes routes and XYZ routes

Let's take a closer look at Feature Routes.

Why Feature Routes?

The big benefit of Feature Routes (should you choose to use them) is that it allows a feature to promote it's screens in an encapsulated and autonomous way!

Feature Routes are based on a very simple concept: allow the redux application state to drive the routes!

In feature based routing, you will not find the typical "route path to component" mapping catalog, where (for example) some pseudo route('signIn') directive causes the SignIn screen to display, which in turn causes the system to accommodate the request by adjusting it's state appropriately. Rather, the appState is analyzed, and if the user is NOT authenticated, the SignIn screen is automatically displayed ... Easy Peasy!

Depending on your perspective, this approach can be more natural, but more importantly (once again), it allows features to promote their own screens in an encapsulated and autonomous way!

How Feature Routes Work

Each feature (that maintains UI screens) promotes it's top-level screens through a Feature.route property (within feature-u's createFeature()).

A route is simply a function that reasons about the redux appState, and either returns a rendered component, or null to allow downstream routes the same opportunity. Basically the first non-null return wins.

If no component is established (after analyzing the routes from all features), the router will revert to a configured fallback - a Splash Screen of sorts (not typical but may occur in some startup transitions).

The route directive contains one or more function callbacks (routeCB()), as defined by the content parameter of featureRoute(). This callback has the following signature:

API: routeCB({fassets, appState}): reactElm || null

Route Priorities

A Feature.route may reference a single routeCB() or an array of multiple routeCB()s with varying priorities. Priorities are integer values that are used to minimize a routes registration order. Higher priority routes are given precedence (i.e. executed before lower priority routes). Routes with the same priority are executed in their registration order.

While priorities can be used to minimize (or even eliminate) the registration order, typically an application does in fact rely on registration order and can operate using a small number of priorities. A set of PRIORITY constants are available for your convenience (should you choose to use them).

Priorities are particularly useful within feature-u, where a given feature is provided one registration slot, but requires it's route logic to execute in different priorities. In that case, the feature can promote multiple routes (an array) each with their own priority.

Here is a route for an Eateries feature (displaying a list of restaurants) that employs two separate routeCB()s with varying priorities:

src/feature/eateries/route.js

import React               from 'react';
import {createFeature}     from 'feature-u';
import {featureRoute,
        PRIORITY}          from 'feature-router';
import * as sel            from './state';
import featureName         from './featureName';
import EateriesListScreen  from './comp/EateriesListScreen';
import EateryDetailScreen  from './comp/EateryDetailScreen';
import EateryFilterScreen  from './comp/EateryFilterScreen';
import SplashScreen        from '~/util/comp/SplashScreen';

export default createFeature({

  name: featureName,

  route: [
    featureRoute({
      priority: PRIORITY.HIGH,
      content({fassets, appState}) {
        // display EateryFilterScreen, when form is active (accomplished by our logic)
        // NOTE: this is done as a priority route, because this screen can be used to
        //       actually change the view - so we display it regardless of the state of
        //       the active view
        if (sel.isFormFilterActive(appState)) {
          return <EateryFilterScreen/>;
        }
      }
    }),

    featureRoute({
      content({fassets, appState}) {

        // allow other down-stream features to route, when the active view is NOT ours
        if (fassets.sel.getView(appState) !== featureName) {
          return null;
        }
        
        // ***
        // *** at this point we know the active view is ours
        // ***
        
        // display annotated SplashScreen, when the spin operation is active
        const spinMsg = sel.getSpinMsg(appState);
        if (spinMsg) {
          return <SplashScreen msg={spinMsg}/>;
        }
        
        // display an eatery detail, when one is selected
        const selectedEatery = sel.getSelectedEatery(appState);
        if (selectedEatery) {
          return <EateryDetailScreen eatery={selectedEatery}/>;
        }
        
        // fallback: display our EateriesListScreen
        return <EateriesListScreen/>;
      }
    }),

  ],

  ... snip snip
});

Feature Order and Routes

The Feature.route aspect may be one rare characteristic that dictates the order of your feature registration. It really depends on the specifics of your app, and how much it relies on Route Priorities.

With that said, it is not uncommon for your route logic to naturally operate independent of your feature registration order.

Routing Precedence

A fundamental principle to understand is that feature based routing establishes a Routing Precedence as defined by your application state!

As an example, an 'auth' feature can take routing precedence over an 'xyz' feature, by simply resolving to an appropriate screen until the user is authenticated (say a SignIn screen or an authorization splash screen during auth processing).

This means the the 'xyz' feature can be assured the user is authenticated! You will never see logic in the 'xyz' feature that redirects to a login screen if the user is not authenticated. Very natural and goof-proof!!!

Configuration

fallbackElm$

routeAspect.config.fallbackElm$ (REQUIRED):

Before you can use routeAspect you must first configure the fallbackElm$ representing a SplashScreen (of sorts) when no routes are in effect. Simply set it as follows:

import {createRouteAspect} from 'feature-router';
import SplashScreen        from './wherever/SplashScreen';

const routeAspect = createRouteAspect();

...
routeAspect.config.fallbackElm$ = <SplashScreen msg="I'm trying to think but it hurts!"/>;
...

This configuration is required, because it would be problematic for feature-router to devise a default. For one thing, it doesn't know your app layout. But more importantly, it doesn't know the react platform in use (ex: react-web, react-native, expo, etc.).

componentWillUpdateHook$

routeAspect.config.componentWillUpdateHook$ (OPTIONAL):

You can optionally specify a <StateRouter> componentWillUpdate life-cycle hook (a function that, when defined, will be invoked during the componentWillUpdate react life-cycle phase). This was initially introduced in support of react-native animation. Simply set it as follows:

import {createRouteAspect} from 'feature-router';
import {LayoutAnimation}   from 'react-native';

const routeAspect = createRouteAspect();

...
routeAspect.config.componentWillUpdateHook$ = () => LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
...

Interface Points

feature-router accumulates all the routes from the various features of your app, and registers them to it's <StateRouter> component. The Aspect Interface to this process (i.e. the inputs and outputs) are documented here.

Input

  • The input to feature-router is the set of routing callback hooks. This is specified by each of your features (that maintain UI Screens) through the Feature.route property, referencing functions defined by the featureRoute() utility.

Exposure

  • feature-router promotes the app's active screen by injecting it's <StateRouter> component at the root of your application DOM. This allows your Feature.route hooks to specify the active screen, based on your application state.

  • As a convenience, feature-router auto injects the feature-u Fassets object as a named parameter in the routeCB() API. This promotes full Cross Feature Communication.

Error Conditions

  • Required Configuration

    If you fail to configure the required fallbackElm$, the following exception will be thrown:

    launchApp() parameter violation: 
    the route aspect requires config.fallbackElm$ to be configured (at run-time)!
    
  • routeAspect Placement (Aspect Order)

    The routeAspect must be ordered before other aspects that inject content in the rootAppElm (i.e. the Aspects passed to launchApp()). The reason for this is that <StateRouter> (the underlying utility component) does NOT support children (by design).

    When feature-router detects this scenario (requiring action by you), it will throw the following exception:

    ***ERROR*** Please register routeAspect (from feature-router) 
                before other Aspects that inject content in the rootAppElm
                ... <StateRouter> does NOT support children.
    
  • NO Routes in Features

    When feature-router detects that no routes have been specified by any of your features, it will (by default) throw the following exception:

    ***ERROR*** feature-router found NO routes within your features
                ... did you forget to register Feature.route aspects in your features?
                (please refer to the feature-router docs to see how to override this behavior).
    

    Most likely this should in fact be considered an error (for example you neglected to specify the routes within your features). The reasoning is: why would you not specify any routes if your using feature-router?

    You can change this behavior through the following configuration:

    routeAspect.config.allowNoRoutes$ = true;

    With this option enabled, when no routes are found, feature-router will simply NOT be configured (accompanied with a WARNING logging probe).

    You can also specify your own array of routes in place of the true value, which will be used ONLY in the scenario where no routes were specified by your features.

API

routeAspect: Aspect

    API: createRouteAspect([name='route']): routeAspect

    The routeAspect is the feature-u plugin that facilitates Feature Route integration to your features.

    To use this aspect:

    • Within your mainline:

      • configure the routeAspect.config.fallbackElm$ representing a SplashScreen (of sorts) when no routes are in effect.

      • register the feature-router routeAspect to feature-u's launchApp().

    • Within each feature that maintains UI Components, simply register the feature's route through the Feature.route property (using feature-u's createFeature()). This Feature.route references a function defined through the featureRoute() utility.

    Please refer to the Usage section for examples of this process.

featureRoute()

    API: featureRoute({content, [priority]}): routeCB

    Embellish the supplied content function (a routeCB()) with a routePriority property (a specification interpreted by Feature Router) as to the order in which the set of registered routes are to be executed.

    A routeCB() reasons about the supplied redux appState, and either returns a rendered component screen, or null to allow downstream routes the same opportunity. Basically the first non-null return wins (within all registered routes).

    Priorities are integer values that are used to minimize a routes registration order. Higher priority routes are given precedence (i.e. executed before lower priority routes). Routes with the same priority are executed in their registration order. While a priority can be any integer number, for your convenience, a small number of PRIORITY constants are provided.

    For more details, please refer to A Closer Look.

    Please Note: featureRoute() accepts named parameters.

    Parameters:

    • content: routeCB()

      The the routeCB() to embellish.

    • [priority]: integer

      The optional priority to use (DEFAULT: PRIORITY.STANDARD or 50).

    Return: routeCB()

      the supplied content function, embellished with the specified routePriority property.

routeCB()

    API: routeCB({fassets, appState}): reactElm || null

    A functional callback hook (specified by featureRoute()) that provides a generalized run-time API to abstractly expose component rendering, based on appState.

    A routeCB reasons about the supplied redux appState, and either returns a rendered component screen, or null to allow downstream routes the same opportunity. Basically the first non-null return (within all registered routes) wins.

    The routeCB also has a routePriority associated with it. Priority routes are given precedence in their execution order. In other words, the order in which a set of routes are executed are 1: routePriority, 2: registration order. This is useful in minimizing the registration order.

    For more details, please refer to A Closer Look.

    Please Note: routeCB() accepts named parameters.

    Parameters:

    • fassets: Fassets object

      The Fassets object used in feature cross-communication.

      SideBar: fassets is actually injected by the routeAspect using <StateRouter>'s namedDependencies. However, since feature-router is currently the only interface to <StateRouter>, we document it as part of this routeCB API.

    • appState: Any

      The top-level redux application state to reason about.

    Return: reactElm || null

      a rendered component (i.e. react element) representing the screen to display, or null for none (allowing downstream routes an opportunity).

PRIORITY

    The PRIORITY container promotes a small number of defined constants. This is strictly a convenience, as any integer can be used.

    Priorities are integer values that are used to minimize a routes registration order. Higher priority routes are given precedence (i.e. executed before lower priority routes). Routes with the same priority are executed in their registration order. While a priority can be any integer number, for your convenience, a small number of PRIORITY constants are provided:

    import {PRIORITY} from 'feature-router';
    
    // usage:
    PRIORITY.HIGH     // ... 100
    PRIORITY.STANDARD // ...  50 ... the default (when NOT specified)
    PRIORITY.LOW      // ...  10

    For more information, please refer to Route Priorities.

Potential Need for Polyfills

The implementation of this library employs modern es2015+ JavaScript constructs. Even though the library distribution is transpiled to es5 (the least common denominator), polyfills may be required if you are using an antiquated JavaScript engine (such as the IE browser).

We take the approach that polyfills are the responsibility of the client app. This is a legitimate approach, as specified by the W3C Polyfill Findings (specifically Advice for library authors).

  • polyfills should only be introduced one time (during code expansion of the app)
  • a library should not pollute the global name space (by including polyfills at the library level)
  • a library should not needlessly increase it's bundle size (by including polyfills that are unneeded in a majority of target environments)

As it turns out, app-level polyfills are not hard to implement, with the advent of third-party utilities, such as babel:

If your target JavaScript engine is inadequate, it will generate native run-time errors, and you will need to address the polyfills. Unfortunately, in many cases these errors can be very obscure (even to seasoned developers). The following Babel Feature Request (if/when implemented) is intended to address this issue.