We’re examining how Angular exposes dynamic component creation and runtime metadata through a surprisingly small API surface. Angular is a component-based framework for building web applications, and deep in its core there’s a single file that acts as the front door for dynamic components. I’m Mahmoud Zalt, an AI solutions architect, and we’ll unpack how this file works as a thin but robust façade over Ivy—and what its design teaches us about building our own public APIs.
Where this API sits in Angular
The file packages/core/src/render3/component.ts in the Angular repo is responsible for two core jobs:
createComponent: programmatically create a component instance and wire it into dependency injection (DI) and the DOM.reflectComponentType: read a component’s metadata (selector, inputs, outputs, content slots, flags) at runtime.
packages/
core/
src/
render3/
component.ts <- public facade for dynamic component creation & reflection
component_ref.ts
def_getters.ts
dynamic_bindings.ts
di/
injector.ts
r3_injector.ts
interface/
type.ts
linker/
component_factory.ts
createComponent(Type, options)
├─ ngDevMode && assertComponentDef(component)
├─ getComponentDef(component)
├─ elementInjector = options.elementInjector || getNullInjector()
├─ new ComponentFactory(componentDef)
└─ factory.create(...)
reflectComponentType(Type)
├─ componentDef = getComponentDef(component)
├─ if !componentDef → return null
├─ factory = new ComponentFactory(componentDef)
└─ return mirror { getters delegate to factory and componentDef }
It doesn’t implement rendering, change detection, or DI itself. Instead, it knows just enough to route calls into the right internal machinery. It’s effectively the receptionist to a huge factory: it takes your request and calls the right machine to either build a component or print its spec sheet.
The primary lesson in this file is how to design a tiny façade that exposes powerful capabilities—dynamic creation and reflection—while keeping the public surface small, defensive, and easy to evolve.
How the façade stays small but safe
Despite doing important work, this file exports only two functions and one interface. Its value comes from how it orchestrates internals and how carefully it shapes what’s visible to consumers.
createComponent as an explicit orchestrator
Here is the core implementation of createComponent:
export function createComponent(
component: Type,
options: {
environmentInjector: EnvironmentInjector;
hostElement?: Element;
elementInjector?: Injector;
projectableNodes?: Node[][];
directives?: (Type | DirectiveWithBindings)[];
bindings?: Binding[];
},
): ComponentRef {
ngDevMode && assertComponentDef(component);
const componentDef = getComponentDef(component)!;
const elementInjector = options.elementInjector || getNullInjector();
const factory = new ComponentFactory(componentDef);
return factory.create(
elementInjector,
options.projectableNodes,
options.hostElement,
options.environmentInjector,
options.directives,
options.bindings,
);
}
The function is intentionally thin. It takes a component type and an options object, then does three things:
- Validate in dev mode with
assertComponentDefso incorrect usage is caught early during development. - Retrieve the compiled definition using
getComponentDef(component), which returns Ivy’s internal descriptor for that component. - Delegate creation by constructing a
ComponentFactoryand callingcreatewith injectors, host element, content projection, directives, and bindings.
The important part is what createComponent does not do. It doesn’t embed rendering logic, DI rules, or any heuristics. It simply orchestrates well-defined collaborators. That keeps the public entry point easy to reason about, test, and adjust as internal details evolve.
Closing the robustness gap
There is a small fragility here: getComponentDef(component)! uses a non-null assertion. In dev mode, assertComponentDef usually prevents invalid types from reaching this point. In production, dev checks may be stripped, and a non-component type could lead to a confusing failure later in the call stack.
A slightly safer variant adds one explicit check without complicating the happy path:
export function createComponent(
component: Type,
options: CreateComponentOptions,
): ComponentRef {
ngDevMode && assertComponentDef(component);
const componentDef = getComponentDef(component);
if (!componentDef) {
throw new Error(
`createComponent() called with a type that is not an Angular component: ${
(component as any)?.name || component
}`,
);
}
const elementInjector = options.elementInjector || getNullInjector();
const factory = new ComponentFactory(componentDef);
return factory.create(
elementInjector,
options.projectableNodes,
options.hostElement,
options.environmentInjector,
options.directives,
options.bindings,
);
}
This keeps the function short while turning a potential undefined access into a clear, actionable error in production. It also motivates extracting the inline options object into a named CreateComponentOptions interface, which improves discoverability and reusability across the codebase and documentation.
ComponentMirror: a narrow reflection surface
On the reflective side, ComponentMirror defines what callers are allowed to see about a component:
export interface ComponentMirror {
get selector(): string;
get type(): Type;
get inputs(): ReadonlyArray<{
readonly propName: string;
readonly templateName: string;
readonly transform?: (value: any) => any;
readonly isSignal: boolean;
}>;
get outputs(): ReadonlyArray<{readonly propName: string; readonly templateName: string}>;
get ngContentSelectors(): ReadonlyArray;
get isStandalone(): boolean;
get isSignal(): boolean;
}
It exposes:
- Selector: the HTML tag or CSS selector.
- Inputs/outputs: with both the class property name and the template binding name.
- Content projection slots: via
ngContentSelectors. - Feature flags:
isStandaloneandisSignal.
This is a deliberately narrow view over richer internal metadata. All members are getters, not mutable properties, which makes the mirror read-only and lets Angular derive values from the underlying definition or factory.
The implementation of reflectComponentType is just as focused:
export function reflectComponentType(component: Type): ComponentMirror | null {
const componentDef = getComponentDef(component);
if (!componentDef) return null;
const factory = new ComponentFactory(componentDef);
return {
get selector(): string {
return factory.selector;
},
get type(): Type {
return factory.componentType;
},
get inputs() {
return factory.inputs;
},
get outputs() {
return factory.outputs;
},
get ngContentSelectors() {
return factory.ngContentSelectors;
},
get isStandalone(): boolean {
return componentDef.standalone;
},
get isSignal(): boolean {
return componentDef.signals;
},
};
}
Two design choices matter here:
- Graceful failure: if
getComponentDefreturns nothing, the function returnsnullinstead of throwing. Reflection is treated as an optional capability. - Encapsulation: callers never receive the raw Ivy definition. They interact with a curated mirror that Angular can extend over time (for example, by adding new getters) without breaking existing consumers.
Tightening the surface area
Summarizing the most instructive refinements you might apply to an API shaped like this:
| Aspect | Current | Improved | Impact |
|---|---|---|---|
createComponent safety |
Relies on dev-only assertComponentDef and non-null assertion |
Explicit runtime check when componentDef is missing |
Clear production errors, faster debugging for misconfiguration |
| Options typing | Inline object type in the function signature | Named CreateComponentOptions interface |
Better IDE discoverability and reuse across docs and call sites |
Behavior under heavy use
The façade itself is lightweight, but how it’s used can have real performance and operational consequences once you scale up.
Where cost actually accumulates
The bodies of createComponent and reflectComponentType do a constant amount of work. They delegate almost everything to ComponentFactory and getComponentDef. The actual cost depends on:
- the complexity of the component’s template and DI graph when creating components, and
- how frequently you instantiate factories or scan components when reflecting.
Two hot paths show up in practice:
- apps that continuously create and destroy components at runtime (dashboards, popup-heavy UIs, shells), and
- tooling that calls
reflectComponentTypeacross many components during startup or analysis.
Metrics that reveal real problems
This file doesn’t emit logs or metrics, but the most useful observability hooks live around its callers. Three metrics are especially informative:
angular_dynamic_component_creations_total– how oftencreateComponentis invoked. Spikes can reveal runaway instantiation or leaks.angular_dynamic_component_creation_duration_ms– end-to-end time to create a component. This captures template complexity and DI costs; a reasonable P95 target is on the order of tens of milliseconds per component on your target devices.angular_component_reflect_calls_total– how heavily reflection is used, especially during startup or navigation.
Because the façade itself is cheap, you’re unlikely to see it as a hotspot in profiles. These metrics help you spot when a previously harmless capability has become a pressure point due to scale or calling patterns.
Repeated factory instantiation
One performance smell is that every call to reflectComponentType creates a new ComponentFactory for the same component type.
For typical app usage this overhead is small. But frameworks or tools that scan hundreds or thousands of components on each run will pay for those allocations repeatedly. A straightforward improvement—either in this façade or in a deeper layer—is to cache factories in a WeakMap. That way, repeated reflections for the same type can reuse the factory without introducing leaks.
Patterns to reuse in your own code
This small Angular core file shows how a tiny façade can safely expose powerful features without leaking internal complexity. It does that by staying thin, validating carefully, and returning curated views instead of raw internals.
Applied to your own libraries and services, the main patterns are:
- Keep entry points thin and explicit. Let them orchestrate dedicated collaborators rather than embed business logic. That makes them simpler to reason about, test, and evolve as internals change.
- Validate early and fail clearly. Combine rich dev-time assertions with minimal runtime checks in places where missing invariants would otherwise cause opaque failures.
- Expose mirrors, not guts. For reflection or introspection, design a narrow, read-only interface—like
ComponentMirror—instead of exposing your internal schema directly. - Give options a name. Use structured options objects, and extract them into named interfaces once the shape stabilizes. This improves IDE help, documentation, and reuse across the codebase.
- Instrument usage, not the wrapper. Attach metrics to how often and how slowly the façade is used, so you can see when scaling patterns turn a cheap call into a systemic cost.
If we design our APIs the way Angular designs createComponent and reflectComponentType, we can keep the top-level surface small and approachable while still driving large, dynamic systems underneath.





