Unleash the Power of Feature Based JS Development — with feature-u V1

Building complex front-end applications with JavaScript can quickly get unwieldy as the codebase grows. The default tendency is to organize code by "type" – components in one folder, redux actions/reducers in another, routes in another, etc.

However, this often leads to tightly coupled code that is hard to maintain and modify. A single feature‘s implementation gets scattered across multiple folders. Different teams working on different features keep stepping on each other‘s toes. Adding new features or modifying existing ones becomes difficult and error-prone.

This is where "feature-based" architecture comes to the rescue! The core idea is to organize your codebase by "feature" rather than "type". Each feature gets implemented in a separate folder, encapsulating all the components, state, routes, etc. that it needs. This makes the codebase much more maintainable and modular.

Feature-based architecture enables:

  • 📦 Features developed as independent, plug-and-play modules
  • 🧩 Mix-and-match reusable features to build different apps
  • 👨‍👨‍👦 Multiple teams to simultaneously work on different features
  • ➕ Easier to add, modify or remove features

Sounds amazing, right? But as you start implementing features in a JavaScript/React codebase, you quickly run into some tricky questions:

  • 🤔 How do I let features communicate without breaking encapsulation?
  • 🤔 How do I compose my UI from components across feature boundaries?
  • 🤔 How do I let features hook into the application lifecycle?
  • 🤔 How do I enable/disable features at runtime?

This is where the hero of our story, feature-u, comes in! It‘s a super helpful library that makes feature-based development in JS a breeze. Let‘s see how it helps us solve these challenges.

Enter feature-u

At its core, feature-u provides a simple Feature object that lets you define a feature‘s public interface. This includes any state, components, routes, or business logic that the feature wants to expose to the rest of the application.

Here‘s a sample feature that exposes a reducer and a component:

import { createFeature } from ‘feature-u‘;

export default createFeature({
  name:     ‘sample‘,
  reducer:  { ‘sample/counter‘: counterReducer },
  fassets: {
    define: {
      ‘sample.component.counter‘: CounterComponent,
    },
  }
});

The createFeature function takes a configuration object where you can define:

  • name: a unique name for your feature
  • reducer: any redux reducer that your feature wants to contribute to the global state
  • fassets: a mapping of keys to assets (components, values, etc.) that your feature wants to expose

The fassets section is the most interesting one – it‘s feature-u‘s mechanism to enable cross-feature communication without breaking encapsulation! We‘ll come back to it in a bit.

In the startup code for your app, you register all your feature modules using feature-u‘s launchApp function:

import { launchApp } from ‘feature-u‘;
import { reducerAspect } from ‘feature-redux‘;
import sampleFeature from ‘./sample‘;

export default launchApp({
  aspects: [
    reducerAspect,
  ],
  features: [
    sampleFeature,
  ],
  registerRootAppElm(rootAppElm) {
    ReactDOM.render(rootAppElm,
                    document.getElementById(‘root‘));
  }
});

The launchApp function takes a config object where you can specify:

  • aspects: any additional plugins your app needs (for state mgmt, routing, etc.)
  • features: the list of feature modules to register
  • registerRootAppElm: a callback to render the root app component

This is the "magic" that feature-u performs on startup:

  • 🏗️ Registers all your features and plugins
  • 🎳 Hooks up your state management (redux), routing, etc. based on plugins
  • 🚀 Calls any app lifecycle hooks exposed by features
  • 🌉 Sets up the public API for cross-feature communication

From your feature‘s perspective, it can simply focus on implementing the functionality it needs, while leveraging the plugin infrastructure setup by feature-u. UI components, state, actions, routes, etc. can all be defined within the feature, without worrying about registering them with the global app.

Cross-feature communication

We briefly mentioned fassets above – this is feature-u‘s killer feature (pun intended 😉) to facilitate cross-feature communication.

Within your feature, any asset (component, value, function, etc.) that you want to expose to the rest of the app can be added to the fassets.define section:

fassets: {
  define: {
    ‘sample.component.counter‘: CounterComponent,
    ‘sample.value.counterLabel‘: ‘My Counter‘,
  }
}

Other features can then "use" these assets via the fassets.use section:

fassets: {
  use: [
    ‘sample.component.counter‘,
  ]
}

And access them at runtime via the withFassets HOC:

function OtherComponent(props) {
  return (
    <div>

      <props.fassets.get(‘sample.component.counter‘) />
    </div>
  );
}

export default withFassets(OtherComponent);

The withFassets HOC automagically injects the requested fassets into your component as props. This enables cross-feature UI composition without breaking encapsulation! Features don‘t directly import components from each other – they simply rely on the fassets facility to expose and consume assets.

In fact, the fassets.use section supports wildcards, allowing your feature to dynamically consume assets matching a pattern. For example:

fassets: {
  use: [
    ‘sample.component.*‘,
  ]
}

This will inject all components exposed by the sample feature as an array into your component via props.fassets. What‘s really powerful is that this allows your feature to be developed in isolation, while still being able to integrate with other dynamically plugged in features!

Feature lifecycle hooks

Another pain point with features is allowing them to hook into the application lifecycle to perform any setup or teardown.

Feature-u solves this quite elegantly by allowing features to define appWillStart and appDidStart hooks:

export default createFeature({
  appWillStart({ curRootAppElm, appConfig, fassets }) {
    // do any pre-startup init
  },

  appDidStart({ curRootAppElm, appConfig, fassets }) {
    // do any post-startup init
  },
})

These hooks are invoked by launchApp during app startup. The appWillStart hook lets you do any pre-initialization (e.g. registering API endpoints), while appDidStart is called post-initialization (e.g. kicking off initial API calls).

The hooks are passed the curRootAppElm (root app component), appConfig (launchApp config), and fassets (assets exposed by features), giving you full flexibility to inspect or mutate the application.

Enabling/disabling features

Finally, feature-u has a neat little feature (I‘m running out of synonyms!) to let you enable/disable features at runtime. Each Feature object can define an enabled flag:

export default createFeature({
  enabled: false,
  // ...
})

Setting enabled: false will prevent the feature from being registered at startup. You can dynamically set this flag based on any runtime condition (e.g. user permissions, A/B test, etc.).

So a typical startup flow would look like:

// configure features
const features = [
  sampleFeature,
  // ...
];

// optionally override enable flag
const enabledFeatures = features.map(feature => {
  feature.enabled = true; // TODO: add runtime check
  return feature;
});

// launch app
launchApp({
  aspects: [
    // ...
  ],
  features: enabledFeatures,
  registerRootAppElm(rootAppElm) {
    // ...
  }
});

This lets you ship all your features in the same JS bundle, while easily toggling them at runtime. Pretty slick!

Putting it all together

Let‘s see how this all comes together with a mini tutorial. We‘ll build a rudimentary shopping app with two pages – a product listing and a cart view. We‘ll implement these as two independent features, with some cross-feature hooks.

Here‘s the product feature:

const productFeature = createFeature({
  name: ‘product‘,

  fassets: {
    define: {
      ‘product.page.list‘: ProductListPage,
      ‘product.component.card‘: ProductCard,
    }
  }
});

function ProductListPage() {
  return (
    <div>

      <ProductCard id={123} />
      {/* ... */}
    </div>
  );
}

function ProductCard({id}) {
  // ...
  return (
    <div>
      <img src={product.img} />
      <h3>{product.name}</h3>
      <p>{product.desc}</p>
      <button onClick={() => addToCart(product)}>Add to Cart</button>
    </div>
  );
}

And here‘s the cart feature:

const cartFeature = createFeature({
  name: ‘cart‘,

  appWillStart({fassets}) {
    apiClient.init();
    Store.init(fassets);
  },

  fassets: {
    define: {
      ‘cart.page.view‘: CartViewPage,
    },

    use: [
      ‘product.component.card‘,
    ]
  }
});

function CartViewPage() {
  const cartItems = useSelector(state => state.cart.items);

  return (
    <div>

      {cartItems.map(item => (
        <ProductCard key={item.id} {...item} />
      ))}
      <button onClick={checkout}>Checkout</button>
    </div>
  );
}

Finally, here‘s the top-level startup code:

launchApp({
  aspects: [
    reducerAspect,
    routerAspect,
  ],

  features: [
    productFeature,
    cartFeature,
  ],

  registerRootAppElm(rootAppElm) {
    ReactDOM.render(
      <Provider store={store}>
        <Router>
          {rootAppElm}
        </Router>  
      </Provider>,
      document.getElementById(‘root‘)
    );
  }
});

This demonstrates a few key points:

  1. The product and cart features are completely independent, self-contained modules.
  2. Cross-feature communication happens via fassets:
    • cart feature "uses" the product.component.card asset exposed by product
    • cart initializes shared dependencies (apiClient, store) on startup
  3. Features can hook into app lifecycle via appWillStart to do any setup
  4. The launchApp function sets up all the top-level providers, initializes plugins, and renders the app
  5. Routing is delegated to the individual page components exposed by features

Conclusion

I hope this gives you a taste of the power and simplicity of feature-based development with feature-u. It really shines in large, complex JavaScript codebases developed by multiple teams.

By treating features as independent, pluggable modules, you can:

  • 🧩 Develop and test features in isolation
  • ⚡ Easily add, modify or remove features
  • 🎨 Improve code reuse and modularity
  • 😌 Simplify app startup and configuration
  • 🤝 Facilitate collaboration across teams

Feature-u provides a thin layer of abstraction and conventions to enable cross-feature communication, while still promoting loose coupling. It doesn‘t lock you into any specific framework, and can work with your favorite tools for state management, routing, etc.

So go forth and build some awesome features! 🚀

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *