feature-u: (Feature Based Project Organization for React)

Kevin Bridges
Mar 12, 2018 · 16 min read
Image for post
Image for post

This article is an introduction to feature-u — a library that facilitates feature-based project organization in your react project. This utility assists in organizing your project by individual features.

Most developers would agree that organizing your project by features 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 many good articles on this topic with insights on feature-based design and structure (see: References below).

This article outlines my excursion into feature-based composition. In working through the details, I realized there was an opportunity for a library to help manage and streamline some of the hurdles incurred in this process. The result: feature-u (check out the full docs, GitHub source, and NPM package).

Update: On 8/14/2018 feature-u V1 was released, that re-designed Cross Feature Communication new to include UI Composition new as a core offering. A new article can be found here that takes a comprehensive approach in introducing you to all of feature-u (including V1). We are very excited about this update, because it promotes one solution for all feature collaboration! While upgrading to V1 requires some client code mods (see V1 Migration Notes), it is well worth it. This article is based on feature-u V0, and is using some antiquated APIs (mostly Feature.publicFace, and the app object). Still, this is a good resource to get your feet wet with feature-u.

At a Glance

feature-u Basics — introduce high-level feature-u concepts

eatery-nod App — the sample app used to demonstrate feature-u

Before & Aftereatery-nod project structure before and after features

feature-u In Action — explore feature-u aspects through concrete examples

feature-u Benefits — in summary

References — feature-based articles

Backdrop

out of the Starting Gate …

In general, I was feeling good about my progress. I was starting to see concrete benefits … feature segregation was going to result in code that is much more manageable!

the Hurdles …

How can I encapsulate and isolate my 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 I promote feature-based UI components in an isolated and autonomous way?

How can I configure my chosen frameworks now that my code is so spread out?

How can I enable/disable selected features which are either optional, or require a license upgrade?

In short, how can I pull it all together so that my individual features operate as one application?

the Goal (what now?)

  1. Allow features to Plug-and-Play! This encompasses many things, such as: encapsulation, cross communication, enablement, initialization, and so on. We will build on these concepts throughout this article.
  2. Automate the startup of your application!! You have the features. Allow them to promote their characteristics, so a central utility can automatically configure the frameworks used in your app, thereby launching your application. This task must be accomplished in an extendable way, because not everyone uses the same set of frameworks.

feature-u Basics

In turn, these Feature objects are supplied to launchApp(), which configures and starts your application running. In addition, the returned App object is exported, in order to promote the public API of each feature.

aspects

Aspects can take on many different forms:

  • UI Components and Routes
  • State Management (actions, reducers, selectors)
  • Business Logic
  • Startup Initialization Code
  • And so on…

Not all aspects are of interest to feature-u — only those that are needed to setup and launch the app — 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.

framework integration

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.

eatery-nod App

eatery-nod randomly selects a “date night” restaurant from a pool of favorites. My wife and I have a steady “date night”, and we are always indecisive on which of our favorite restaurants to frequent :-) So eatery-nod provides the spinning wheel!

Take a look at the eatery-nod README to get a feel for the application. Screen flows are available, so it really helps in your orientation to the project.

Image for post
Image for post
eatery-nod’s primary screen flow

In addition, README files are found in each feature, describing what each feature accomplishes. Take some time now and skim through these resources:

  • device — initializes the device for use by the app, and promotes a device API abstraction
  • auth — promotes complete user authentication
  • leftNav — promotes the app-specific Drawer/SideBar on the app’s left side
  • currentView — maintains the currentView with get/set cross-feature communication bindings
  • eateries — manages and promotes the eateries view
  • discovery — manages and promotes the discovery view
  • firebase — initializes the Google Firebase service
  • logActions — logs all dispatched actions and resulting state
  • sandbox — promotes a variety of interactive tests, used in development, that can easily be disabled

Before & After

Image for post
Image for post

Let’s take a look at eatery-nod’s directory structure (before/after).

For illustration purposes, I have only expanded a few directories, but I think you get the idea.

Before: here is my project's before features

eatery-nod src BEFORE features

src/
├──actions/ ... redux actions
│ auth.js
│ discovery.js
│ eateries.js
│ ... snip snip
├──api/ ... various abstract APIs
│ device.js
│ discovery.js
│ ... snip snip
├──app/ ... mainline startup **1**
│ │ ScreenRouter.js
│ │ SideBar.js
│ │ index.js
│ └──startup/
│ │ createAppStore.js
│ │ platformSetup.android.js
│ │ platformSetup.ios.js
│ └──firebase/
│ firebaseAppConfig.js
│ initFireBase.js
├──appState/ ... redux reducers
│ auth.js
│ discovery.js
│ eateries.js
│ ... snip snip
├──comp/ ... UI Component Screens
│ DiscoveryListScreen.js
│ EateriesListScreen.js
│ ... snip snip
├──logic/ ... redux-logic modules
│ auth.js
│ discovery.js
│ eateries.js
│ ... snip snip
└──util/ ... common utilities

After: and here is the same project's after features

eatery-nod src AFTER features

src/
│ app.js ... launches app via launchApp() **2**
├──feature/
│ │ index.js ... accumulate/promote all app Feature objects
│ ├──auth/ ... the app's authorization feature
│ │ │ actions.js
│ │ │ featureName.js
│ │ │ index.js
│ │ │ logic.js
│ │ │ publicFace.js
│ │ │ route.js
│ │ │ signInFormMeta.js
│ │ │ state.js
│ │ └──comp/
│ │ SignInScreen.js
│ │ SignInVerifyScreen.js
│ ├──currentView/ ... other features
│ ├──device/ ... feature to initialize the device
│ │ │ actions.js
│ │ │ api.js
│ │ │ appDidStart.js
│ │ │ appWillStart.js
│ │ │ featureName.js
│ │ │ index.js
│ │ │ logic.js
│ │ │ publicFace.js
│ │ │ route.js
│ │ │ state.js
│ │ └──init/
│ │ platformSetup.android.js
│ │ platformSetup.ios.js
│ ├──discovery/ ... more features
│ ├──eateries/
│ ├──firebase/
│ ├──leftNav/
│ ├──logActions/
│ └──sandbox/
└──util/ ... common utilities used across all features

As expected, the difference in project organization is dramatic!

  • Before features — you find constructs for a given feature spread over numerous typed directories.
  • After features: all aspects of a given feature are contained in its own isolated directory.
  • A notable difference is the dramatic reduction in complexity of the application startup process! The “before features” contained an entire app\ directory of startup code (see **1** above), while the "after features" simply contains a single app.js startup file (see **2** above). Where did all the complexity go? ... stay tuned!

feature-u In Action

Image for post
Image for post

Each of the following sections briefly introduce a new feature-u topic, correlating sample code from eatery-nod. Additional information is provided through links, both to the feature-u docs, and eatery-nod source code. In some cases the in-lined sample code has been streamlined (to emphasize a focal point), however the caption link will take you to the actual code (hosted on GitHub).

Here are our topics …

  1. Simplified App Startup
  2. React Platforms
  3. Feature Object
  4. Feature Initialization
  5. Feature Collaboration
  6. Framework Integration
  7. Feature Enablement
  8. Managed Code Expansion
  9. UI Component Promotion
  10. Single Source of Truth

1. Simplified App Startup

To solve this, feature-u provides the launchApp() function (see: Launching Your Application).

Here is eatery-nod’s mainline …

src/app.js

The first thing to note is just how simple and generic the mainline startup process is. There is no real app-specific code in it … not even any global initialization!

That is because feature-u provides various hooks that allow your features to 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. (*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. (*2*)all app features are accumulated from our feature/ directory
  3. (*3*)as a preview to Feature Collaboration, the exported return value of launchApp() is an App object, which promotes the accumulated Public API of all features.

2. React Platforms

Here are some registerRootAppElm() variations:

react web:

react web registerRootAppElm()

react-native:

react-native registerRootAppElm()

expo:

expo registerRootAppElm()

3. Feature Object

Here is an example from eatery-nod’s device feature.

src/feature/device/index.js

As you can see, the Feature object is merely a container that holds aspect content of interest to feature-u. The sole purpose of the Feature object is to communicate this aspect information to launchApp().

We will fill in more detail a bit later, but for now notice that the feature is conveying reducers, logic modules, routes, and does some type of initialization (appWillStart/appDidStart). It also promotes a publicFace that can be used by other features (such as the feature’s Public API).

For more information, please refer to Feature & aspect content.

4. Feature Initialization

This could be any number of things, such as:

  • initialize some service API
  • inject a utility react component at the App root
  • dispatch an action that kicks off a startup process
  • And more...

To solve this, feature-u introduces two Application Life Cycle Hooks, injected through the following Feature aspects:

  1. Feature.appWillStart({app, curRootAppElm}): rootAppElm || falsy Invoked one time, just before the app starts up. This can do any type of initialization, including supplementing the app's top-level root element (such as the React component instance).
  2. Feature.appDidStart({app, appState, dispatch}): void
    Invoked one time immediately after the app has started. A typical usage for this hook is to dispatch some type of bootstrap action.

Here are some examples from eatery-nod:

FireBase Initialization:

src/feature/firebase/index.js

Bootstrap Action:

src/feature/device/appDidStart.js via index.js

Inject DOM Root Elm:

src/feature/leftNav/appWillStart.js via index.js

5. Feature Collaboration

To solve this, feature-u promotes a Cross Feature Communication. This is accomplished through the Feature.publicFaceBuilt-In aspect property. A feature can expose whatever it deems necessary through its publicFace. There are no real constraints on this resource. It is truly open.

Typically this involves promoting selected:

  • actions
  • selectors
  • APIs
  • And so on

The publicFace of all features are accumulated and exposed through the App object (emitted from launchApp()).

It contains named feature nodes, as follows:

App.{featureName}.{publicFace}

Here is an example from eatery-nod’s auth feature.

src/feature/auth/publicFace.js via index.js

Out of all the items found in the auth feature, only two actions and one selector are public.

Here is what the App object would look like for this example:

app: {
auth: {
actions: {
userProfileChanged(userProfile),
signOut(),
},
sel: {
getUserPool(appState),
},
},
currentView: { // other features
... snip snip
},
}

As a result, the auth feature's public API can be accessed as follows:

app.auth.actions.userProfileChanged(userProfile)
app.auth.actions.signOut()
app.auth.sel.getUserPool(appState)

6. Framework Integration

To solve this, feature-u introduces Extendable aspects. feature-u is extendable. It provides integration points between your features and your chosen frameworks.

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). They are created with feature-u’s extendable API, using createAspect(). You can define your own Aspect, if the one you need doesn't already exist.

Let’s take a look at a redux example from eatery-nod.

The device feature maintains its own slice of the state tree.

It promotes its reducer through the Feature.reducer aspect:

src/feature/device/index.js

Because Feature.reducer is an extended aspect (verses a built-in aspect), it is only available because we registered the feature-redux reducerAspect to launchApp() (please refer to Simplified App Startup above).

The key thing to understand is that feature-u (through the feature-redux extension) will automatically configure redux by accumulating all feature reducers into one overall appState.

Here is the reducer code …

src/feature/device/state.js

A feature-based reducer is simply a normal reducer that manages the feature’s slice of the the overall appState. The only difference is it must be embellished with slicedReducer(), which provides instructions on where to insert it in the overall top-level appState.

As a result, the device reducer only maintains the state relevant to the device feature (like its little slice of the world) — a status, a fontsLoaded indicator, and the device location.

SideBar: We are using the astx-redux-util utility’s reducerHash() function to concisely implement the feature's reducer (providing an alternative to the common switch statement). I have found that in using a utility like this, for most cases it is feasible to implement all the reducers of a feature in one file (due in part to the smaller boundary of a feature). astx-redux-util also promotes other Higher-Order Reducers. You may want to check this out.

7. Feature Enablement

To solve this, feature-u introduces Feature Enablement. Using the Feature.enabled Built-In aspect (a boolean property), you can enable or disable your feature.

Here is an example from eatery-nod’s sandbox feature:

src/feature/sandbox/index.js

The sandbox feature promotes a variety of interactive tests, used in development, that can easily be disabled.

Typically this indicator is based on a dynamic expression, but in this case it is simply hard-coded (to be set by a developer).

SideBar: When other features interact with a feature that can be disabled, you can use the App object to determine if a feature is present or not (see: Feature Enablement for more information).

8. Managed Code Expansion

To solve this, feature-u introduces Managed Code Expansion.

When aspect content definitions require the App object at code expansion time, you simply wrap the definition in a managedExpansion() function. In other words, your aspect content can either be the actual content itself (such as a reducer), or a function that returns the content.

When this is done, feature-u will expand it by invoking it in a controlled way, passing the fully resolved App object as a parameter.

Here is a logic module from eatery-nod’s auth feature:

src/feature/auth/logic.js

You can see that the auth feature is using an action from the device feature, requiring access to the app object (see *2*). Because the app object is needed during code expansion, we use the managedExpansion() function (see *1*), allowing feature-u to expand it in a controlled way, passing the fully resolved app object as a parameter.

9. UI Component Promotion

To address this, feature-u recommends considering Feature Based Routes via the feature-router extendable Aspect (packaged separately). This approach can even be used in conjunction with other navigational solutions.

Feature Routes are based on a very simple concept: allow the application state to drive the routes! It operates through a series of feature-based routing functions that reason about the appState, and either return a rendered component, or null to allow downstream routes the same opportunity.

Here is a simple example from the device feature.

This route analyzes the current appState, and displays a SplashScreen until the system is ready:

src/feature/device/route.js via index.js

In feature based routing, you will not find the typical “route path to component” mapping catalog, where (for example) some pseudo route('device') directive causes the Device screen to display, which in turn causes the system to accommodate the request by adjusting its state appropriately.

Rather, the appState is analyzed, and if the device is NOT ready, no other screens are given the option to even render ... Easy Peasy!

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

10. Single Source of Truth

What are some Best Practices for single-source-of-truth as it relates to features, and how can feature-u help?

The Best Practices section highlights a number of feature-based single-source-of-truth items of interest. These are guidelines, because you must implement them in your application code (feature-u is not in control of this).

Here is an example from the eateries feature:

src/feature/eateries/state.js via index.js

The featureName is used to specify the top-level state location of this feature (see *1*). feature-u guarantees the feature name is unique. As a result, it can be used to qualify the identity of several feature aspects.

For example:

  • prefix action types with featureName, guaranteeing their uniqueness app-wide (see: feature-redux docs)
  • prefix logic module names with featureName, identifying where that module lives (see: feature-redux-logic docs)
  • depending on the context, the featureName can be used as the root of your feature state’s shape (see: feature-redux docs)

Because feature-u relies on slicedReducer() (in the feature-redux package), a best practice is to use the reducer's embellished selector to qualify your feature state root in all your selector definitions. As a result the slice definition is maintained in one spot (see *2*).

feature-u Benefits

  • Feature Encapsulation — isolating feature implementations improves code manageability
  • Cross Feature Communication — a feature’s public API is promoted through a well-defined standard
  • Feature Enablement — enable/disable features through a run-time switch
  • Application Life Cycle Hooks — features can initialize themselves without relying on an external process
  • Single Source of Truth — is facilitated in a number of ways within a feature’s implementation
  • Framework Integration — configure the framework(s) of your choice (matching your run-time-stack) using feature-u’s extendable API
  • UI Component Promotion — through Feature Routes
  • Minimize Feature Order Dependency Issues — even in code that is expanded in-line
  • Plug-and-Play — features can be added/removed easily
  • Simplified Mainline launcApp() starts the app running by configuring the frameworks in use, all driven by a simple set of features.
  • Operates in any React Platform (including React Web, React Native and Expo)

Hopefully this article gives you a feel for how feature-u can improve your project. Please refer to the full documentation for more details.

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

References

freeCodeCamp.org

This is no longer updated.

Kevin Bridges

Written by

A “current” developer from a different era (70’s UMR alum) … from the greater St. Louis area (Metro East) … https://wiiBridges.com/

freeCodeCamp.org

This is no longer updated. Go to https://freecodecamp.org/news instead

Kevin Bridges

Written by

A “current” developer from a different era (70’s UMR alum) … from the greater St. Louis area (Metro East) … https://wiiBridges.com/

freeCodeCamp.org

This is no longer updated. Go to https://freecodecamp.org/news instead

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store