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 Aspect
s integrate
feature-u to other framework/utilities that match your specific
run-time stack.
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.
-
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).
-
Within your mainline, register the feature-router
routeAspect
(see:**1**
) to feature-u'slaunchApp()
.Please note that
routeAspect
has a requiredfallbackElm
parameter (see:**2**
) that specifies a reactElm to use when no routes are in effect (a SplashScreen of sorts).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'; export default launchApp({ aspects: [ createRouteAspect({ // **1** and **2** fallbackElm: <SplashScreen msg="I'm trying to think but it hurts!"/> }), createReducerAspect(), // **3** ... other Aspects here ], features, registerRootAppElm(rootAppElm) { ReactDOM.render(rootAppElm, getElementById('myAppRoot')); } });
-
Within each feature that promotes UI Screens, simply register the feature's route through the
Feature.route
property (using feature-u'screateFeature()
).Here is a route for a
startup
feature that simply promotes a SplashScreen until the system is ready. It's route references arouteCB()
(see**4**
) defined through thefeatureRoute()
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 singlefeatureRoute()
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!
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.
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!
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
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
});
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.
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!!!
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.
- 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 thefeatureRoute()
utility.
-
feature-router promotes the app's active screen by injecting it's
<StateRouter>
component at the root of your application DOM. This allows yourFeature.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 therouteCB()
API. This promotes full Cross Feature Communication.
-
routeAspect Placement (Aspect Order)
The
routeAspect
must be ordered before other aspects that inject content in the rootAppElm (i.e. the Aspects passed tolaunchApp()
). 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 by specifying the
allowNoRoutes
constructor parameter (see:routeAspect: Aspect
):createRouteAspect({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.
-
name: The name of this Aspect Plugin (defaults to 'route')
-
fallbackElm: The required reactElm to render when no routes are in effect (a SplashScreen of sorts).
This 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.).
-
componentDidUpdateHook: an optional
<StateRouter>
componentDidUpdate life-cycle hook. When defined, it is a function that is invoked during the componentDidUpdate react life-cycle phase. This was initially introduced in support of react-native animation ... for example:import {createRouteAspect} from 'feature-router'; import {LayoutAnimation} from 'react-native'; createRouteAspect({ ... other params (snip snip) componentDidUpdateHook: () => LayoutAnimation.configureNext(LayoutAnimation.Presets.spring) });
-
allowNoRoutes: an optional boolean expression that determines how to handle the situation where NO Routes were found in the active feature set ... please refer to the NO Routes in Features discussion in Error Conditions.
-
Within your mainline, register the feature-router
routeAspect
to feature-u'slaunchApp()
.The
fallbackElm
constructor parameter is required, and represents a SplashScreen (of sorts) when no routes are in effect. -
Within each feature that maintains UI Components, simply register the feature's route through the
Feature.route
property (using feature-u'screateFeature()
). ThisFeature.route
references a function defined through thefeatureRoute()
utility.
API: createRouteAspect({name:'route',
fallbackElm,
componentDidUpdateHook,
allowNoRoutes:false}): routeAspect
The routeAspect
is the feature-u plugin that facilitates
Feature Route integration to your features.
PARAMS: (Please Note: only named parameters are used)
USAGE:
Please refer to the Usage section for examples of this process.
-
content:
routeCB()
The the
routeCB()
to embellish. -
[priority]: integer
The optional priority to use (DEFAULT:
PRIORITY.STANDARD
or 50).
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:
Return: routeCB()
the supplied content
function, embellished with the specified
routePriority
property.
-
fassets:
Fassets object
The
Fassets object
used in feature cross-communication.SideBar:
fassets
is actually injected by therouteAspect
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.
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:
Return: reactElm || null
a rendered component (i.e. react element) representing the screen to display, or null for none (allowing downstream routes an opportunity).
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.
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:
- simply import babel-polyfill
- or use babel's
babel-preset-env
in conjunction with babel 7's
"useBuiltins": "usage"
option
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.