DEV Community

Cover image for How To Build Angular Components Teams Actually Reuse (No More Copy-Paste Hell)
Karol Modelski
Karol Modelski

Posted on • Originally published at javascript.plainenglish.io

How To Build Angular Components Teams Actually Reuse (No More Copy-Paste Hell)

Ever built a “reusable” modal in Angular that felt like a win at first? You know, the one everyone high-fived over because it slashed duplicate code across your app. Fast forward six months, and it’s a tangled beast — props everywhere, endless tweaks breaking unrelated features, and your team dreading every Jira ticket involving it.

It’s a classic trap. Reusable components sound smart: extract common UI like buttons or forms into shared libraries, especially in Nx monorepos where everything’s neatly organized. The DRY principle (“Don’t Repeat Yourself”) pushes us to generalize early. But here’s the rub — overdoing it creates bloated abstractions that handle every edge case imaginable. Suddenly, a simple dialog morphs into a maintenance monster, slowing your team down instead of speeding them up. It’s like that Swiss Army knife gadget that’s got 47 tools but sucks at opening bottles.

This happens because we chase reusability before knowing the real patterns. Duplication isn’t always evil; sometimes it’s cheaper than wrestling the wrong abstraction.

In this article, we’ll unpack smart architecture with Nx libraries, a straightforward cost-benefit lens for generic versus domain-specific components, and fresh Angular tricks like signals and dependency injection to ditch prop-drilling hell. No Nx setup tutorials or Angular 101 here — just battle-tested patterns.

If you’re on an engineering team scaling Angular — from coders in the trenches to tech leads plotting the big picture, even stakeholders eyeing ROI — this is your roadmap to sustainable code that actually ships faster. Let’s fix the reusability myth together.


The Hidden Cost of “Reusable” Components

Think of building a Lego castle where you craft one super-versatile brick designed to fit every possible angle, twist, and glow effect. It sounds brilliant — until that brick doesn’t snap just right for the turret, forcing awkward hacks with a knife. That’s premature abstraction in UI development: a “reusable” component promising efficiency but delivering frustration.

This chapter uncovers why rushing to generalize hurts codebases more than it helps. Whether you’re an engineer wrestling with bloated APIs or a manager tracking dev hours, you’ll gain a shared language to weigh costs against real benefits. In Angular apps powered by signals, the temptation feels even stronger — but the pitfalls run deeper.

The Trap of the Over-Generic Modal

Premature abstraction strikes when you build overly generic UI components before true reuse needs emerge. Take a modal dialog: It starts simple for confirmations, but optimism bloats it with signal declarations like mode: signal<'success' | 'error' | 'warning' | 'info'>(), iconPosition: signal<'top' | 'left'>(), customFooterTemplate: signal<TemplateRef>(), and loadingStates: signal<string[]>(). Dozens of inputs. Complex output emitters for every event. It compiles. It works... once.

The problems pile up fast. Developers face a cognitive marathon to use it: “Does loadingStates need three items? Is iconPosition mandatory?" Integration becomes signal-wrangling hell. Coupling tightens—tweak one signal's logic, and unrelated screens break. Defect risk soars from untested "just in case" branches.

It’s like trying to fix a car while it’s still running: You aim to streamline, but layers of unnecessary complexity slow everything down. In signal-based Angular, this bloat masquerades as modern reactivity, yet it creates the same old maintenance traps.

A Cost Model You Can Actually Use

Reusability has a price tag. Frame it simply: initial build time + integration complexity + maintenance overhead + defect risk, balanced against actual reuse (often just 1–2 spots, not dozens).

Cost comparison: Premature generic components vs. concrete domain-specific ones<br>

This table cuts through the hype. Managers see lost hours quantified. Engineers spot the math behind “just duplicate it.” Premature generics rarely pay off — they cost more to create and sustain than targeted copies.

The Rule of Three: Your New Best Friend

That’s where the Rule of Three comes in — a practical heuristic from software craftsmanship. First implementation? Solve concretely, no frills. Second time around? Copy-paste guilt-free or note duplicates. Third occurrence? Abstract, now guided by real variation points.

Why three? It surfaces genuine patterns without premature guesses. In Angular’s standalone components, extraction shines: Hoist shared signal logic while keeping domain code lean. The result? A crisp, reality-tested API — no regrets, just focused power.

From Fiasco to Fix: A Classic Cautionary Tale

Consider a typical fintech team building a “super modal” for checkout confirmations: mode: signal<'pay' | 'cancel' | 'retry'>(), dynamic button signals, flag overload. It launches in checkout, then admin panels, onboarding—three domains hit. Success?

Not quite. Checkout requires payment iframes (embedMode: signal<boolean>() plus hacks). Admin demands bulk actions (buttons explode into queryContent() messes). Onboarding adds animations (more flag conditionals). Six months later: PR reviews stretch from 10 to 45 minutes, bugs double, teams dodge it.

The fix? Domain-specific modals first: CheckoutConfirmModal, AdminBulkModal, OnboardingStepper. Each uses minimal signals, nails its job. By the third, patterns emerge: title, content, actions. Extract a lean DialogBase with title: "signal<string>(), content: signal<TemplateRef>(), actions: signal<DialogAction[]>()."

Outcome: Seamless reuse, halved bugs, 30% faster iterations. No hacks. Clean, battle-tested code.

Take Control, Build Smarter

Premature abstraction trades short-term “wow” for long-term pain. In signal-driven Angular UIs, it’s especially sneaky — reactivity invites overgeneralization. Pause next time: Has the Rule of Three kicked in? Are variations proven?

Managers, reward duplication twice, abstraction thrice. Engineers, prioritize concrete wins. Together, craft maintainable magic over monstrous “reusables.” Your codebase — and deadlines — will breathe easier.

Structuring Reuse with Nx Libraries: From Chaos to Clean Code

Think of your Angular codebase like a shared toolbox. At first, tossing everything into one bin works fine for quick fixes. But as projects grow, you end up digging through a jumble of screws, hammers, and half-broken gadgets. Finding the right tool? Nightmare. That’s the trap of ad-hoc shared folders — they start simple but morph into bloated “god libraries” where UI bits tangle with business logic. Nx libraries flip this script. They organize reuse into structured UI, feature, and domain libraries, aligning perfectly with your app’s boundaries for Angular v21. No more mess. Just scalable sanity.

The Problem: Why Shared Folders Fail at Scale

Shared folders seem harmless: drop a button component here, a utility there. But without boundaries, they expand wildly. A modal for orders sneaks in checkout logic. Soon, everything depends on everything, breaking modularity. Features leak into domains; refactoring turns into a dependency hunt. It’s like a toolbox overflowing onto the floor — functional until it’s not.

Nx libraries solve this with purpose-built types, generated via CLI like nx g @nx/angular:lib shared/ui-modal. Built for Angular 21's standalone components (zoneless ready), they enforce clean separation from the start. Ditch folders. Embrace structure.

Nx Library Types: Mapped to Bounded Contexts

Nx defines three key types, each fitting a bounded context:

  • Shared UI Libraries (libs/shared/ui-*): For "dumb" components—pure visuals like buttons, modals, form fields. They consume input() data and emit output() events. No injected services. Leaf nodes: usable everywhere, depending only on utils or other UI.
  • Feature Libraries (libs/orders/feat-*): "Smart" components for specific workflows, such as order lists or checkout flows. They wire up data from domain or data-access layers. Depend on UI and domain, but not the reverse.
  • Domain Libraries (libs/orders/domain): Core business rules—models, validators, logic. Often include +state for NgRx. Pure and isolated, depending solely on utils. No UI contamination.

Add tags on generation: nx g @nx/angular:lib orders/domain --tags=type:domain,scope:orders. It's a natural hierarchy: UI at the base, domain at the core, features on top.

Deciding Where Components Belong

Placement boils down to one question: “Is this generic visual pattern or workflow-specific?”

Quick decision tree for Nx libraries: generic vs. workflow-specific.

The flow: Start in a feature or domain library. Prove reuse across multiple areas? Promote to shared UI (strip smarts, add inputs). Nx’s move generator handles the refactor seamlessly. Like sorting a toolbox—specialty tools stay put, universals go front and center.

Enforcing Discipline: Tags and Dependency Rules

Rules without enforcement? Useless. Nx’s @nx/enforce-module-boundaries ESLint plugin uses project.json tags to block bad imports at lint time. Sample rules in eslint.config.js:

{ sourceTag: 'type:domain', onlyDependOnLibsWithTags: ['type:util'] },
{ sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:ui', 'type:domain', 'type:util'] },
{ sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] }
Enter fullscreen mode Exit fullscreen mode

Violate? Build fails. Domain stays pure — no feature dependencies. UI remains leaf-like. No “god” libraries hoarding your codebase.

E-Commerce Example: A Practical Workspace

Consider an e-commerce Nx setup: shop app pulling from libs/shared/ui-modal, libs/orders/feat-order-management, and libs/checkout/feat-checkout.

  1. Develop OrderDetailsModal in orders/feat-order-management—smart, data-aware.
  2. Need similar for checkout? Build CheckoutSummaryModal there.
  3. Spot overlap? Extract to shared/ui-modal (dumb version: input() for data).
  4. Apply tags: orders scope depends on shared; UI as leaf.
  5. Lazy-load routes: { path: 'orders', loadChildren: () => import('@org/orders/feat-order-management') }.

The modal “graduates” from specific to shared, scaling reuse effortlessly.

The Bigger Win: Apps That Grow Gracefully

Nx libraries aren’t folders — they’re architecture guardrails. Teams collaborate without chaos; boundaries hold firm; refactors become routine. Like a well-organized toolbox, everything has its place, ready for any job. Your Angular monorepo evolves from fragile to robust. Next shared component? Skip the folder. Generate a library. Watch the magic.

⚙️ Stuck on a complex migration?
I help teams with focused “Component Detox” sessions to untangle legacy code and implement modern patterns.

👉 See how I can help →

Angular Micro-Engagements & Development | Karol Modelski | Freelance Portfolio

Eliminate frontend bottlenecks with fixed-price Angular micro-engagements. Access specialized expertise for Audits, Refactors, and Feature Builds without the hourly overhead.

favicon karol-modelski.scale-sail.io

Ditching Prop Drilling: Angular Signals and DI to the Rescue

Imagine you’re building a fancy modal dialog in your Angular app. You’ve got a parent component that knows the theme (dark mode or light?), the size (compact or full-screen?), and whether it should close on Escape. Simple enough. But then you nest a header, body, and footer inside — and suddenly, every single one needs that same config info. You start passing it down: input() here, another there, and before you know it, you're buried in a chain of props snaking through five levels of components. Sound familiar? That's prop drilling, and it's the silent killer of clean, reusable Angular code.

I’ve been there. Last year, I refactored a dashboard with nested charts, and what started as a sleek feature turned into a prop-passing nightmare. Tweaking one config meant updating a dozen intermediate components. It felt like trying to whisper a secret through a crowded room — by the time it reached the end, it was garbled, and everyone in between was annoyed.

The Prop Drilling Trap

Prop drilling happens when data meant for a deeply nested component has to hop through every parent along the way. In traditional Angular, you’d declare inputs with input() on each middleman, bloating their interfaces with stuff they don't even use. Reuse? Forget it. Your pristine button component now demands a "theme" input it ignores, just to pass it along.

It’s not just ugly — it’s brittle. Change the parent’s API, and ripple effects hit everywhere. Components lose their independence, turning your app into a house of cards. Angular v21 flips the script with signals and dependency injection (DI). No more handoffs. Components grab what they need directly, like pulling ingredients from a shared pantry instead of begging the chef at every station.

Signals: Reactive Config, Zero Boilerplate

Enter signals — Angular’s fine-grained reactivity superpower. Forget @Input() decorators. With input(), you define reactive values right in the class:

theme = input<'dark' | 'light'>('light');
size = input<'sm' | 'lg'>('sm');
closeOnEsc = input<boolean>(true);
Enter fullscreen mode Exit fullscreen mode

These are signals: readable with theme(), writable reactively, and they trigger updates only where needed. No OnChanges lifecycle hooks. No zone.js drama. Pair them with computed() for derived state:

styleClass = computed(() => 
  this.theme() === 'dark' ? 'dark-bg' : 'light-bg'
);
Enter fullscreen mode Exit fullscreen mode

It’s like giving your components a live feed of data — changes propagate surgically, keeping things fast and zoneless-ready.

But signals alone don’t solve deep nesting. That’s where DI shines.

Supercharge with DI: Provide Once, Inject Anywhere

Angular’s DI tree is your secret weapon. Define an InjectionToken for your config:

import { InjectionToken, Signal, computed, inject } from '@angular/core';

interface ModalConfig {
  theme: 'dark' | 'light';
  size: 'sm' | 'lg';
  closeOnEsc: boolean;
}

export const MODAL_CONFIG = new InjectionToken<Signal<ModalConfig>>('MODAL_CONFIG');
Enter fullscreen mode Exit fullscreen mode

In your feature module or parent component’s providers, create the signal:

providers: [{
  provide: MODAL_CONFIG,
  useFactory: () => {
    const parent = inject(ParentModalComponent);
    return computed(() => ({
      theme: parent.theme(),
      size: parent.size(),
      closeOnEsc: parent.closeOnEsc()
    }));
  }
}]
Enter fullscreen mode Exit fullscreen mode

Now, any descendant — header, footer, or a button three levels deep — injects it effortlessly:

@Component({
...,
host: {
    '(document:keydown.escape)': 'onEscape($event)'
  },
})
export class ModalCloseButton {
  private readonly config = inject(MODAL_CONFIG, { optional: true });
  private readonly modalService = inject(ModalService);

  readonly shouldCloseOnEsc = computed(() => this.config()?.closeOnEsc ?? true);

  onEscape(event: Event): void {
    if (this.shouldCloseOnEsc()) {
      event.preventDefault();
      this.close();
    }
  }

  close(): void {
    this.modalService.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

Intermediate components? Blissfully ignorant. No inputs. No outputs. The <modal-header> doesn't care about theme — but if it did, it'd inject the same signal. It's scoped magic: provide globally at the app root, override locally in a feature.

A Real-World Modal Makeover

Let’s walk through that configurable modal. The parent <app-modal> exposes input() signals for theme, size, and close behavior. DI then packages them into MODAL_CONFIG. The header reads size for its layout. The body derives theme classes with a computed(). A nested close button checks the config signal to decide if it should listen for Escape. Zero prop chains anywhere.

Want dark mode? Flip the parent’s signal — everything re-renders surgically. Need to reuse the modal in a different library? Just provide fresh config at the boundary. It’s like LEGO blocks with invisible wiring that always connects perfectly.

In a typical multi-tenant admin panel, you’ll see modals, tooltips, and drawers all driven from a shared tenant configuration signal via DI. Teams that adopt this pattern often cut refactor time dramatically and eliminate those endless “who needs this prop?” meetings.

Why This Changes Everything

This isn’t just a trick; it’s a paradigm shift. Signals make state reactive and composable. DI scopes it perfectly, from app-wide themes to feature-specific tweaks. Your components stay pure, focused, and reusable — the Angular way, evolved.

In v21, with signal queries and outputs in the mix, you’re set for zoneless futures. Prop drilling becomes a relic, like fax machines in 2026. Next time you’re passing props through purgatory, pause. Inject a signal. Watch the chains vanish.

Your app will thank you — and so will your future self over that coffee.

Conclusion

If there’s one big takeaway from exploring sustainable Angular architectures, it’s this: abstraction should earn its place. We’ve all written that one “reusable” component — packed with inputs, outputs, and edge cases — only to realize it’s reused nowhere. It’s a rite of passage for every developer. The trick isn’t to stop abstracting but to time it right. Strong architecture isn’t about endless flexibility; it’s about clear boundaries, and Nx gives us the tools to defend them beautifully.

Angular v21 makes this balance even easier to strike. With signals and Dependency Injection, we can create components that adapt without crumbling under complexity. The framework is nudging us toward a more intentional way of building — where reactivity feels organic, and configuration doesn’t mean chaos. It’s a great reminder that sustainable code isn’t clever for clever’s sake — it’s code that stays useful, understandable, and alive as your app evolves.

So, here’s your challenge: pick one “reusable” component in your project. Audit it. Ask what problem it really solves, who benefits, and whether it’s doing too much. Then simplify it. Refactor it. Make it domain-first and let patterns emerge naturally again. You might be surprised by how much faster everything moves once the clutter clears.

Because in the end, great architecture isn’t about chasing perfection — it’s about building momentum, releasing friction, and letting your ideas flow freely through code. That’s how sustainable software — and satisfied developers — are made.


Need a Senior Angular Dev, but don’t have the headcount?

You don’t need a full-time hire to fix a slow dashboard or migrate a critical module to Signals. I specialize in “surgical” Angular interventions.

I help teams with:

  • 🔍 Code Quality Audits: Find out exactly why your app is slow.
  • 🧩 Component Detox: Refactor complex legacy components to modern standards.
  • 🔌 API Integration Layers: Build robust data services that scale.

⏱️ No long onboarding.
❌ No long-term contracts.
✅ Just solved problems.

👉 Book a Code Quality Audit →

Top comments (0)