PFC #2 - Page-Feature Composition: Pages as Orchestrators
How to compose features without losing your mind
TL;DR
Pages are conductors, not implementers. They compose features into user experiences through simple templates, handle navigation and routing, and manage feature-to-feature communication when needed. Keep pages under 50 lines of TypeScript by letting features own all business logic.
In PFC #1, we introduced the core philosophy: separate features (business logic) from pages (orchestration). Now, let’s dive deep into how pages actually work as orchestrators.
Remember that messy 200-line page component mixing users, products, and orders? By the end of this post, you’ll know exactly how to break it down into a clean, composable architecture.
Series Context: This is Part 2 of the Page-Feature Composition series. New here? Start with PFC #1 to understand the foundation.
The Conductor Mindset
Think about a symphony orchestra. The conductor doesn’t play violin, trumpet, or drums. They coordinate musicians to create a cohesive performance.
Pages work the same way. They don’t implement user management, product catalogs, or order processing. They coordinate features to create user experiences.
What Pages Should Do
✅ Compose features - Arrange feature containers on the screen
✅ Handle navigation - Respond to route changes and manage routing
✅ Coordinate communication - Pass data between features when needed
✅ Apply layout - Structure the page visually
✅ Manage route-specific state - Track active tab, scroll position, etc.
What Pages Should NOT Do
❌ Business logic - That belongs in features
❌ API calls - Features handle their own data
❌ Complex state management - Features manage their own state
❌ Component implementation - Use feature containers, don’t build inline
Golden rule: If your page component has more than 50 lines of TypeScript, something probably belongs in a feature.
Page Anatomy
A typical page is mostly template, minimal code:
// src/app/pages/dashboard/dashboard.page.ts
@Component({
templateUrl: ‘./dashboard.html’,
styleUrls: [’./dashboard.scss’]
})
export class DashboardPage {
constructor(private router: Router) {}
navigateToUserDetail(userId: string): void {
this.router.navigate([’/users’, userId]);
}
}That’s eight lines. The template does the heavy lifting:
<!-- src/app/pages/dashboard/dashboard.html -->
<div class=”dashboard-grid”>
<section class=”users-section”>
<h2>Team Overview</h2>
<app-user-management-container
mode=”compact”
[maxItems]=”8”>
</app-user-management-container>
</section>
<section class=”products-section”>
<h2>Latest Products</h2>
<app-product-catalog-container
mode=”summary”>
</app-product-catalog-container>
</section>
</div>The template is declarative. It says, “Put user management here, products there.” That’s orchestration.
Composition Patterns
Let’s explore the most common patterns for composing features.
Pattern 1: Grid Layout
Multiple features arranged in a grid. The most common pattern.
<div class=”grid-layout”>
<div class=”grid-item span-2”>
<app-user-management-container mode=”compact”></app-user-management-container>
</div>
<div class=”grid-item”>
<app-notifications-container></app-notifications-container>
</div>
<div class=”grid-item span-3”>
<app-analytics-container></app-analytics-container>
</div>
</div>Grid styling in CSS. Features just get composed.
Pattern 2: Tabs
Different features on different tabs. Page manages which tab is active.
export class AdminPage {
activeTab = ‘users’;
}<nav class=”tabs”>
<button (click)=”activeTab = ‘users’”>Users</button>
<button (click)=”activeTab = ‘products’”>Products</button>
</nav>
<app-user-management-container *ngIf=”activeTab === ‘users’”></app-user-management-container>
<app-product-catalog-container *ngIf=”activeTab === ‘products’”></app-product-catalog-container>Tab state lives on the page. Features don’t know about tabs.
Pattern 3: Master-Detail
One feature drives what another displays.
export class CustomerPortalPage {
selectedUserId: string | null = null;
}<div class=”master-detail”>
<aside class=”master”>
<app-user-list-container
(userSelected)=”selectedUserId = $event”>
</app-user-list-container>
</aside>
<main class=”detail”>
<app-user-detail-container
*ngIf=”selectedUserId”
[userId]=”selectedUserId”>
</app-user-detail-container>
</main>
</div>User list emits selection. Page coordinates are passed to the detail view.
Pattern 4: Multi-Step Flow
A wizard with different features at each step.
export class OnboardingPage {
currentStep = 1;
}<div class=”steps”>Step {{currentStep}} of 3</div>
<app-profile-form-container
*ngIf=”currentStep === 1”
(completed)=”currentStep = 2”>
</app-profile-form-container>
<app-preferences-container
*ngIf=”currentStep === 2”
(completed)=”currentStep = 3”>
</app-preferences-container>Step management is a page concern. Each step is a feature.
Feature Communication
Sometimes, features need to communicate with each other. The page coordinates.
Simple: Through the Page
export class SalesDashboardPage {
selectedDateRange: DateRange | null = null;
}<app-date-picker-container
(dateRangeChanged)=”selectedDateRange = $event”>
</app-date-picker-container>
<app-sales-chart-container
[dateRange]=”selectedDateRange”>
</app-sales-chart-container>
Date picker emits. Page passes to the chart. Simple and explicit.
Advanced: Using Facades
When you need to trigger actions programmatically:
export class ProductManagementPage implements OnInit {
constructor(
private route: ActivatedRoute,
private productFacade: ProductCatalogFacade
) {}
ngOnInit(): void {
this.route.params.subscribe(params => {
if (params[’category’]) {
this.productFacade.filterByCategory(params[’category’]);
}
});
}
}Page responds to routes and triggers feature actions via the facade. Container still handles the UI.
Common Mistakes
Mistake 1: Business Logic in Pages
Don’t do this:
export class DashboardPage {
users: User[] = [];
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users.filter(u => u.active);
});
}
}Do this instead:
export class DashboardPage {
// Just compose features in template
}Let the user-management feature handle users.
Mistake 2: Tightly Coupled Features
Don’t do this:
<app-user-list [detailComponentRef]=”userDetail”></app-user-list>
<app-user-detail #userDetail></app-user-detail>Features shouldn’t know about each other.
Do this instead:
<app-user-list (userSelected)=”selectedId = $event”></app-user-list>
<app-user-detail [userId]=”selectedId”></app-user-detail>Page coordinates the connection.
Mistake 3: Complex Page State
Don’t do this:
export class DashboardPage {
userFilters = { ... };
productFilters = { ... };
sortOptions = { ... };
// 50 more properties
}Do this instead:
export class DashboardPage {
activeTab = ‘overview’; // Minimal page state only
}Filters and sort belong in features, not pages.
Real Example
Here’s what a real admin dashboard looks like with PFC:
// 15 lines total
export class AdminDashboardPage {
constructor(private router: Router) {}
onUserSelected(userId: string): void {
this.router.navigate([’/users’, userId]);
}
}<!-- Template composes 4 features -->
<div class=”dashboard-grid”>
<section class=”stats”>
<app-analytics-container mode=”summary”></app-analytics-container>
</section>
<section class=”users”>
<h2>Recent Users</h2>
<app-user-management-container
mode=”compact”
(userSelected)=”onUserSelected($event)”>
</app-user-management-container>
</section>
<section class=”products”>
<h2>Top Products</h2>
<app-product-catalog-container mode=”summary”></app-product-catalog-container>
</section>
<section class=”activity”>
<app-activity-feed-container></app-activity-feed-container>
</section>
</div>Total TypeScript: 15 lines
Features composed: 4 (analytics, users, products, activity)
Business logic in page: 0
Clean, maintainable, testable.
The Page Checklist
Before you mark a page “done,” ask yourself:
Is the TypeScript file under 50 lines?
Does it contain zero business logic?
Are all API calls in features, not the page?
Is state management delegated to features?
Are feature containers doing the heavy lifting?
Is the template declarative and easy to scan?
If you answered “no” to any of these, refactor something into a feature.
Summary
Pages as orchestrators are the foundation of PFC. Keep them lean and focused on composition.
Key takeaways:
✅ Pages compose features via templates
✅ Keep TypeScript under 50 lines
✅ Handle navigation and routing only
✅ Coordinate feature communication when needed
✅ Let features own all business logic
✅ Use composition patterns (grid, tabs, master-detail, steps)
The beauty of this approach? Six months from now, you’ll open a page file and instantly understand what it does. No archaeological dig required.
What’s Next
PFC #3 - Building Bulletproof Features
Internal feature architecture. Containers vs components, the facade pattern, state management, and what to export vs keep private.
PFC #4 - From Monolith to Composition
Practical migration strategies. How to adopt PFC in existing projects without rewriting everything. Team workflows and common pitfalls.
PFC #5 - Real-World Examples
Complete feature and page implementations working together. Dashboard example, form flows, and handling complex scenarios.
Your Challenge
Look at one of your current page components:
How many lines of TypeScript does it have?
How much of that could move into features?
Which composition pattern would work best?
Share your findings in the comments. Next week, we’ll build features the right way.
Part 2 of the Page-Feature Composition series. Follow along at better-frontend.dev for practical Angular architecture.
Previous: PFC #1 - The Philosophy
Next: PFC #3 - Building Features
This concludes the Page-Feature Composition series. Follow better-frontend.dev for more practical Angular architecture insights.
The Series:
- PFC #1 - The Philosophy
- PFC #2 - Pages as Orchestrators ← You are here
- PFC #3 - Building Features
- PFC #4 - Migration Strategies
- PFC #5 - Real-World Examples

