Skip to main content
المدونة

Zalt Blog

Deep Dives into Code & Architecture

AT SCALE

The Tiny API That Powers Dynamic Angular

By محمود الزلط
Code Cracking
15m read
<

Most people think Angular’s dynamic behavior comes from a huge surface area. This piece breaks down the tiny API that actually powers it.

/>
The Tiny API That Powers Dynamic Angular - Featured blog post image

MENTORING

1:1 engineering mentorship.

Architecture, AI systems, career growth. Ongoing or one-off.

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 }
This file sits between public APIs and render3/Ivy internals.

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:

  1. Validate in dev mode with assertComponentDef so incorrect usage is caught early during development.
  2. Retrieve the compiled definition using getComponentDef(component), which returns Ivy’s internal descriptor for that component.
  3. Delegate creation by constructing a ComponentFactory and calling create with 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: isStandalone and isSignal.

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:

  1. Graceful failure: if getComponentDef returns nothing, the function returns null instead of throwing. Reflection is treated as an optional capability.
  2. 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 reflectComponentType across 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 often createComponent is 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, ComponentFactory>. 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:

  1. 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.
  2. Validate early and fail clearly. Combine rich dev-time assertions with minimal runtime checks in places where missing invariants would otherwise cause opaque failures.
  3. Expose mirrors, not guts. For reflection or introspection, design a narrow, read-only interface—like ComponentMirror—instead of exposing your internal schema directly.
  4. 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.
  5. 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.

Full Source Code

Direct source from the upstream repository. Preview it inline or open it on GitHub.

packages/core/src/render3/component.ts

angular/angular • main

Choose one action below.

Open on GitHub

Thanks for reading! I hope this was useful. If you have questions or thoughts, feel free to reach out.

Content Creation Process: This article was generated via a semi-automated workflow using AI tools. I prepared the strategic framework, including specific prompts and data sources. From there, the automation system conducted the research, analysis, and writing. The content passed through automated verification steps before being finalized and published without manual intervention.

Mahmoud Zalt

About the Author

I’m Zalt, a technologist with 16+ years of experience, passionate about designing and building AI systems that move us closer to a world where machines handle everything and humans reclaim wonder.

Let's connect if you're working on interesting AI projects, looking for technical advice or want to discuss anything.

Support this content

Share this article