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 featurereducer
: any redux reducer that your feature wants to contribute to the global statefassets
: 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 registerregisterRootAppElm
: 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:
- The
product
andcart
features are completely independent, self-contained modules. - Cross-feature communication happens via
fassets
:cart
feature "uses" theproduct.component.card
asset exposed byproduct
cart
initializes shared dependencies (apiClient
,store
) on startup
- Features can hook into app lifecycle via
appWillStart
to do any setup - The
launchApp
function sets up all the top-level providers, initializes plugins, and renders the app - 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! 🚀