Subtitle: How to keep an older Angular CDK / Material stack alive after Angular 22 removes factory-based component creation — without rewriting your entire UI layer overnight.
Tags: Angular · Angular 22 · CDK · Material · Migration · postinstall · Compatibility Shim
Audience: Frontend engineers upgrading Angular core while temporarily pinning @angular/cdk / @angular/material to an older major.
Table of contents
- The problem in one sentence
- Why this combination is common
- What Angular 22 actually removed
- How the crash shows up
- Architecture of the fix
- Step-by-step implementation guide
- What each patch does (deep dive)
- How the runtime flow works (visual)
- Pros and cons
- Best practices when you force this compatibility
- Corner cases you must keep in mind
- Exit strategy: how to leave the shim behind
- Checklist before you ship
- Conclusion
1. The problem in one sentence
You upgraded Angular core to 22, but you still need CDK / Material 16. The app builds (or almost builds), then dies at runtime when Overlay, Dialog, Menu, SnackBar, or Tabs try to inject APIs that no longer exist in @angular/core@22.
That mismatch is not a bug in your feature code. It is a version skew between framework internals and an older UI toolkit that still speaks the pre-Ivy factory language.
2. Why this combination is common
Teams often need to split a migration into two tracks:
| Track | Goal | Risk if delayed |
|---|---|---|
| Core / build / TypeScript | Stay on a supported Angular major, Node, and TS | Security, ecosystem lag, CI failures |
| CDK / Material / UI rewrite | Move off legacy Material APIs, theming, and breaking CSS | Large visual + a11y + regression surface |
Material major upgrades are rarely “bump the package and ship.” They touch:
- Legacy Material modules (
MatLegacy*) - Theme / typography / density
- Overlay-heavy flows (dialogs, menus, snackbars, bottom sheets)
- Custom CDK portals and tooltips
So a deliberate temporary state looks like:
@angular/core → 22.x
@angular/cdk → 16.x (pinned)
@angular/material → 16.x (pinned)
Peer dependency warnings are expected. Runtime breakage is the real blocker.
Important: This article describes a temporary compatibility bridge, not a long-term architecture. Treat the shim as debt with an owner and a removal date.
3. What Angular 22 actually removed
Angular has been steering away from factory-based dynamic components since Ivy. In Angular 22, the cleanup became a hard break:
-
ComponentFactoryResolver— removed from the public API -
ComponentFactory— removed - The old
ViewContainerRef.createComponent(factory, index, …)overload — gone
Official direction (paraphrased from Angular’s breaking-change notes):
Pass the component class directly to
ViewContainerRef.createComponent, or use the standalonecreateComponent()function.
Old pattern (CDK 16 still uses this)
const resolver = this.componentFactoryResolver;
const factory = resolver.resolveComponentFactory(MyComponent);
viewContainerRef.createComponent(
factory,
index,
injector,
projectableNodes,
);
New pattern (Angular 22+)
viewContainerRef.createComponent(MyComponent, {
index,
injector,
projectableNodes,
});
Or, when there is no ViewContainerRef:
createComponent(MyComponent, {
environmentInjector: appRef.injector,
elementInjector: customInjector,
projectableNodes,
});
CDK / Material 16’s portal and overlay stack still follows the old path. Angular 22 follows only the new path. Something has to translate between them.
4. How the crash shows up
Typical symptoms after a “core-only” upgrade:
Symptom A — DI token is undefined
TypeError: Cannot read properties of undefined (reading 'hasOwnProperty')
Why: Overlay / portal code still declares an injection dependency on ComponentFactoryResolver. In Angular 22 that token is missing, so DI effectively injects undefined, then blows up inside the injector.
Symptom B — Dialog / Menu / Tooltip attach fails later
Even if you somehow get past bootstrap, attaching a component through DomPortalOutlet or CdkPortalOutlet can fail because createComponent is called with a factory, not a Type.
Symptom C — SnackBar content is empty or throws
MatSnackBar often goes through CdkPortalOutlet. That path is easy to miss if you only patched DomPortalOutlet.
Symptom D — “It works until I restart / reinstall”
If you hand-edit node_modules without a postinstall script, the next clean install wipes the fix. Always automate the patch.
5. Architecture of the fix
The workable temporary approach has three layers:
Mental model
| Layer | Role |
|---|---|
| Shim in core | Makes the token exist again for dependency injection |
| Portal patches | Makes component creation use Angular 22’s Type-based API |
| postinstall | Makes the bridge repeatable on every clean install / CI machine |
Without all three, you usually fix one crash and discover the next on the first snackbar or dialog.
6. Step-by-step implementation guide
Step 0 — Decide this is temporary debt
Write down:
- Why Material cannot move yet
- Who owns the follow-up Material upgrade
- Target removal sprint / release
If you cannot answer those, do not ship the shim.
Step 1 — Pin versions intentionally
Keep core and CDK majors explicit in package.json:
{
"dependencies": {
"@angular/core": "^22.1.1",
"@angular/cdk": "16.2.14",
"@angular/material": "16.2.14"
}
}
Prefer an exact CDK / Material pin (no ^) while the shim exists, so a minor CDK bump does not silently invalidate your string replacements.
Use whatever peer-deps strategy your team already uses (legacy-peer-deps, overrides, etc.). Peer warnings alone are not the fix — the runtime bridge is.
Step 2 — Confirm the failure is the factory gap
Reproduce with a minimal Overlay / Dialog / SnackBar path. Confirm the stack involves:
-
@angular/cdk/overlayor@angular/cdk/portal - Missing / undefined
ComponentFactoryResolver - Or
createComponentreceiving a factory
Step 3 — Add an idempotent postinstall patch script
Create something like:
scripts/patch-angular-22-cdk-compat.mjs
Design rules:
- Idempotent — running twice must be a no-op (use unique markers).
- Fail loud — if Angular / CDK bundle shape changes, throw instead of silently doing nothing.
-
No secrets — only patch known public package files under
node_modules. - Deterministic — same input packages → same output.
Wire it:
{
"scripts": {
"postinstall": "node scripts/patch-angular-22-cdk-compat.mjs"
}
}
Step 4 — Shim ComponentFactoryResolver back onto @angular/core
Conceptually:
- Open
node_modules/@angular/core/fesm2022/core.mjs - Insert a small class that:
- Is
providedIn: 'root' - Implements
resolveComponentFactory(componentType)using Angular’s internal component definition helpers still present in the bundle
- Is
- Re-export
ComponentFactoryResolverfrom the public export list
This restores the DI token. It does not by itself make every portal attach path correct.
Step 5 — Patch CDK portal attach paths
In node_modules/@angular/cdk/fesm2022/portal.mjs, update three call sites:
DomPortalOutletwithViewContainerRefDomPortalOutletwithoutViewContainerRef-
CdkPortalOutlet(SnackBar and template portals)
Replace factory-based creation with Type-based creation.
Step 6 — Clear stale prebundles
Angular’s Vite-based serve cache can keep old prebundled CDK copies. After patching, delete the relevant Vite deps cache under .angular/cache (or restart with a clean cache) so ng serve loads patched files.
Step 7 — Verify the high-risk UI surfaces
At minimum:
- [ ] App bootstrap
- [ ] MatDialog open / close
- [ ] MatMenu / select overlays
- [ ] MatSnackBar with custom component content
- [ ] MatBottomSheet / sidenav overlays if used
- [ ] Custom CDK
Overlay+ComponentPortaldirectives - [ ] Tabs that dynamically create content
Step 8 — Document the debt in the repo
In README or an ADR:
- Why the shim exists
- Which Angular / CDK versions it targets
- How to remove it when Material is upgraded
- Who to ping when
postinstallstarts failing
7. What each patch does (deep dive)
Patch A — Core shim (DI compatibility)
Problem: CDK Overlay injects ComponentFactoryResolver. Token missing → crash at construction.
Fix: Provide a root injectable with the same name/token and a resolveComponentFactory method.
What it must not do:
- Invent a second Angular runtime
- Monkey-patch unrelated DI tokens
- Hide unrelated upgrade errors
Patch B — DomPortalOutlet + ViewContainerRef
Before (CDK 16):
componentRef = portal.viewContainerRef.createComponent(
componentFactory,
portal.viewContainerRef.length,
portal.injector || portal.viewContainerRef.injector,
portal.projectableNodes || undefined,
);
After (Angular 22-compatible):
componentRef = portal.viewContainerRef.createComponent(portal.component, {
index: portal.viewContainerRef.length,
injector: portal.injector || portal.viewContainerRef.injector,
projectableNodes: portal.projectableNodes || undefined,
});
Patch C — DomPortalOutlet without ViewContainerRef
Some attaches (including certain snackbar / overlay container paths) call componentFactory.create(...).
After:
componentRef = createComponent(portal.component, {
environmentInjector: this._appRef.injector,
elementInjector: portal.injector || this._defaultInjector || Injector.NULL,
projectableNodes: portal.projectableNodes || undefined,
});
Remember to import createComponent from @angular/core in the portal bundle if it is not already imported.
Patch D — CdkPortalOutlet
This is the path many teams miss. SnackBar custom content often fails here even after DomPortalOutlet looks “fixed.”
Replace:
resolver.resolveComponentFactory(portal.component);
viewContainerRef.createComponent(componentFactory, ...);
With:
viewContainerRef.createComponent(portal.component, { ...options });
8. How the runtime flow works (visual)
Before the bridge (broken)
After the bridge (working)
Install-time flow
Prefer also opening the interactive companion visualization in Cursor if you are reading this from the engineering workspace — it animates the same flows step by step.
9. Pros and cons
Pros
| Benefit | Why it matters |
|---|---|
| Unblocks Angular 22 core upgrade | Security / ecosystem / TypeScript 6 alignment |
| Avoids a big-bang Material rewrite | UI risk stays in a dedicated follow-up |
| Localized change | Mostly node_modules patching + one script |
| CI-reproducible |
postinstall applies on every clean install |
| Incremental verification | You can validate Overlay surfaces before Material migration |
Cons
| Cost | Why it hurts |
|---|---|
| Unsupported configuration | Angular does not promise this pairing |
| Fragile string patches | CDK / core FESM shape can change on patch releases |
| Hidden debt | New hires may not know why postinstall exists |
| Dual mental models | App code on Angular 22 APIs; UI kit on Angular 16 APIs |
| Cache footguns | Stale Vite prebundles look like “random” regressions |
| Slows Material urgency | Temporary fixes become permanent if no removal plan |
Bottom line: Great as a bridge. Expensive as a destination.
10. Best practices when you force this compatibility
These practices matter precisely because the solution is forceful.
1. Pin exact CDK / Material versions
Do not float on ^16.2.14 while the patch depends on exact source strings.
2. Keep the patch script boring and strict
- Unique markers for every edit
- Throw on unexpected file shape
- Log success / already-applied / failure clearly
- Never “best effort” swallow errors in CI
3. Treat postinstall failures as build failures
If the shim cannot apply, fail the pipeline. Shipping an unpatched tree is worse than a red build.
4. Add a smoke suite for Overlay surfaces
Automate dialog, menu, snackbar, and custom portal opens in CI. Unit tests alone rarely catch this.
5. Separate “core upgrade” PRs from “Material upgrade” PRs
Reviewers need a clean story:
- PR A: Angular 22 + compatibility bridge
- PR B: Material / CDK upgrade + delete bridge
6. Do not spread shims into application code
Prefer patching the boundary (core export + CDK portal) over sprinkling fake providers across feature modules. One bridge is easier to delete than twenty local workarounds.
7. Record version matrix in an ADR
Example:
Supported bridge matrix:
@angular/core@22.1.x
@angular/cdk@16.2.14
@angular/material@16.2.14
Anything else = re-verify or remove the script.
8. Plan the removal before merge
A shim without a removal ticket is how temporary hacks become folklore.
9. Prefer official upgrades when feasible
If Material can move in the same quarter, do that instead. The bridge is for when UI migration truly cannot keep pace with core.
10. Watch Angular / CDK release notes every bump
Even a core patch release can reshape FESM output enough to break naive replacements.
11. Corner cases you must keep in mind
SnackBar custom component content
Often usesCdkPortalOutlet, not onlyDomPortalOutlet. Patch both.Portals without ViewContainerRef
The no-VCR branch needscreateComponent(...)with anenvironmentInjector. Easy to miss.Custom application code still using
ComponentFactoryResolver
Search yoursrc/too. The shim may hide app debt that should be migrated properly.Legacy Material modules
MatLegacy*can surface more Overlay paths. Test legacy and non-legacy entry points.Multiple Angular copies
If a dependency bundles another@angular/core, your shim may patch the wrong copy. Deduplicate.pnpm / Yarn / npm differences
Patch paths assume classicnode_modules/@angular/...layout. Adapt for PnP or isolated linkers.Docker / CI clean installs
Locally patched trees lie. Always validate from a freshnpm ci.Vite / esbuild prebundle cache
After patching, clear Angular’s Vite deps cache or you will debug ghosts.SSR / non-browser platforms
If you use Angular Universal or similar, verify overlay-heavy routes on server and client.Animations module presence
Overlay behavior can differ if animations providers are missing or duplicated.Zone vs zoneless
Timing bugs in overlays can look like “compat failures” under zoneless experiments. Isolate variables.Partial upgrades
Updating only@angular/materialbut not@angular/cdk(or the reverse) can reintroduce skew inside the UI toolkit itself.Marker collisions
If you fork the script across repos, keep marker comments unique and stable.Security / supply chain reviews
Some orgs flagpostinstallmutation ofnode_modules. Document intent for AppSec.Developer onboarding
“Delete node_modules and reinstall” must remain a safe recovery path — which is why idempotentpostinstallmatters.
12. Exit strategy: how to leave the shim behind
When you are ready:
- Upgrade
@angular/cdkand@angular/materialto a major aligned with your Angular core. - Run Material migrations / theme updates.
- Remove
postinstallpatch script and markers. - Grep the repo for shim markers and
ComponentFactoryResolverleftovers. - Delete ADR “temporary” section or mark it superseded.
- Re-run the Overlay smoke suite on a clean install.
Success looks like:
npm ci
# no compatibility patch needed
ng serve / ng build / e2e overlays → green
13. Checklist before you ship
- [ ] Angular core on 22.x; CDK / Material pinned exactly
- [ ] Idempotent
postinstallpatch committed - [ ] Core CFR shim applies on clean install
- [ ] DomPortalOutlet with-VCR path patched
- [ ] DomPortalOutlet no-VCR path patched
- [ ] CdkPortalOutlet path patched
- [ ] Vite / Angular dependency cache strategy documented
- [ ] Dialog, Menu, SnackBar, custom Overlay smoke-tested
- [ ] CI fails if patch cannot apply
- [ ] ADR + removal ticket exist
- [ ] No confidential internal names required to understand the script
14. Conclusion
Angular 22’s removal of ComponentFactoryResolver is the right long-term cleanup. CDK / Material 16 still living on factory-based portals is the short-term reality for many large apps.
You can bridge that gap by:
- Restoring a minimal DI-compatible shim in
@angular/core - Rewriting CDK portal attach calls to Angular 22’s Type-based
createComponent - Running that bridge from
postinstallso every machine gets the same result
Do it deliberately. Pin versions. Fail loud. Test overlays. And schedule the Material upgrade that makes the bridge unnecessary.
Temporary compatibility is engineering. Permanent unsupported skew is risk.
References
- Angular Update Guide
- Angular version compatibility
- Angular PR: remove
ComponentFactoryResolverfrom API surface - Angular deprecations history (factory APIs)
This article describes a general compatibility pattern for Angular 22 + CDK/Material 16. Adapt paths and markers to your package manager and bundle layout. Validate on a clean install before production.




Top comments (0)