In 2026, why does Angular still show the old login state on the home page after I change it in a lazy-loaded module?
If you change the login state inside a lazy-loaded module but the home page still reads the old value, in most cases it is not an API or state library problem — the same service is being provided twice: a lazy-loaded module creates its own child injector, and if providers is declared again there, it gets a new module-scoped instance, and the copy in the root injector is not shared automatically. In 2026 delivery work this shows up most often with login state, global configuration, and cross-module caches. Checking where the service is provided is usually faster than changing business code; in most cases you just remove the duplicate provider, while the few cases that genuinely need module isolation should keep it explicitly and document the boundary.
First, understand that Angular injectors resolve services hierarchically
Angular dependency injection does not keep a single instance for the whole app; it looks up the injector hierarchy from the bottom up. By default, a service with providedIn: 'root' is registered on the root injector and is usually a singleton across the app. But once you enter a lazy-loaded module, that module creates its own child injector, and when resolving a service it checks its own providers first — if there is a hit, it stops looking upward.
So to tell whether two places get the same instance, look at where it is provided, not at the class name. The same name does not mean the same source; written into different injectors they are two copies, and changing one does not move the other.
- Root injector: services with providedIn: 'root', or provided in the root module and app bootstrap configuration, usually shared across the whole app.
- Module injector: the lazy-loaded route module's own providers, visible only inside that module and its child components.
- Component injector: component-level providers, valid only inside that component and its child components; when the component is destroyed, the instance is destroyed with it.
In 2026 standalone components and standalone APIs are used more, and where a service is provided is more explicit than in earlier patterns, but the hierarchy logic has not changed: the closer the provider is to the consumer, the more likely you are to get a local instance.
Why a lazy-loaded module ends up with an extra service instance
A lazy-loaded module creates its own injector only when it first loads. If its providers list declares the same service again, Angular treats it as needing its own instance, and the child injector creates a new one. The result: you change state inside the lazy-loaded module and the root component cannot see it; the root component changes state and the lazy-loaded module does not follow.
This kind of problem is fairly hidden during integration testing: the build reports no error and type checking passes. The usual symptoms are login state failing in some second-level module, or global configuration reading old values on a lazy-loaded page, and it is easily misdiagnosed as API caching, a misused state management library, or a timing issue — head in the wrong direction and you lose half a day or more.
There is a related but separate case to keep apart: the service itself exists only once, but the lazy-loaded module reads the value into a component field before the instance is available, and when the instance updates the component does not re-read it. That is not a multiple-instance problem but a read-timing problem, and the fix is different — the first changes where the service is provided, the second changes how you subscribe or read the value. Recommended order: check the instance first, then check the timing.
Three checks: locate first, then touch the code
Following the usual order in 2026 project delivery, when data differs on two sides you can run these three steps first, rather than switching architecture right away.
- Check the declarations: search the whole project for the service class name, see which providers lists it appears in, and focus on the module behind the lazy-loaded route — do not just search the file you have open.
- Check the instance: log an instance marker in the service constructor (a timestamp or an incrementing counter both work), inject the service in the root component and in the lazy-loaded module, and compare — different markers basically confirm multiple instances.
- Check the timing: confirm the lazy-loaded module's load and preload strategy, and see whether load order combined with multiple instances is producing the stale read.
Two cautions: do not leave the instance marker in the production build; and for step three, look at the preload strategy together with it, otherwise you may treat a timing problem as an instance problem and fix the wrong place.
Once confirmed, how to fix it: comparing a few common approaches
Once you confirm multiple instances, the fix depends on whether the business needs a separate instance. If it should be shared globally, remove the duplicate provider; if it should be isolated per module, declare it explicitly on the module and state a clear boundary. The approaches below are common in delivery work; the change size and regression time are experience ranges, and the actual figure depends on project size and how widely the service is called.
- Option 1: provide only in the root and remove providers from the lazy-loaded module. Good for services that should be singletons, such as login state, global configuration, and cross-module caches; small change, experience range roughly 0.5 to 2 hours including regression.
- Option 2: keep configuration-style services on the root side using the module-provided convention, and have the lazy-loaded module only import the module without providing again. Good for services with initialization parameters; experience range roughly 2 to 6 hours, depending on how many configuration items there are.
- Option 3: keep the lazy-loaded module's own providers and draw a clear isolation boundary through naming or component-level provision. Good for multi-tenant setups and per-module caches; the cost depends on how widely it is called, experience range roughly 1 to 3 days.
- Option 4: introduce a state management library for unified management. Good for medium-to-large projects with complex state and multiple contributors; the introduction and migration cost is higher, typical range 1 to 2 weeks, and small projects usually do not need it.
The point of the comparison is not which approach is more "advanced" but whether the sharing scope of the state is written down clearly. When in doubt, fall back to a singleton with Option 1 first, then isolate gradually as the business requires — that keeps the regression surface under control better than starting with a heavyweight solution.
When it genuinely makes sense to keep two instances
Not every duplicate instance is a problem. Some scenarios really do need each module to hold its own state — tenant-isolated local caches, temporary data computed independently per module, form drafts that only serve one page. In those cases the job is to put the boundary into the code, not to let it happen by accident.
- Worth doing: provide it explicitly on that module or component and note in a comment that a module-level instance is needed here, so whoever comes later can tell.
- Worth doing: pull the shared part into a root-provided service and leave the isolated part to the module, keeping the two responsibilities separate.
- Not recommended: achieving isolation by "providing it once more somewhere else" — readers of the code can hardly see the intent, and whoever takes over may delete it by mistake.
On-site integration: the rework caused by one duplicate provider
A common set of constraints looks like this: a three-to-four-week iteration, two frontend developers splitting the work, and the API contract still changing. Each person writes their own module; the login state service is provided once at the root, and the orders lazy-loaded module, to save effort, provides it again in providers. During integration the orders module keeps getting the initial user ID, and it takes most of a day to trace it to the duplicate provider. The cost: edits to imports in three modules and one call site, plus another regression round, eating one to two days of the integration window.
Based on experience ranges, unifying the provider location at the root from the start usually saves anywhere from a few hours to a day or two of debugging and rework. Check where the service is provided before writing business logic — it pays off compared with tracking it down afterwards.
Where this applies and where it does not
Angular's hierarchical injection exists to isolate services in a modular way; it is not the source of the problem — casually adding providers one more time is. The rule of thumb can be reduced to one sentence: does this state need to change across modules together? If so, provide it at the root; if not, declare it explicitly on the module or component.
- Root provision fits: login state, permission info, global configuration, cross-module caches — duplicating the provider for these is usually the trap.
- Module provision fits: multi-tenant caches, temporary data isolated per module — but write the boundary down explicitly.
- Does not fit: piling every service into the root injector. Startup bundle size and initialization cost go up, and services that should load on demand should still follow their module.
- Does not fit: providing the service one more time just to make a test pass — the test environment and production behavior easily diverge, and it becomes harder to trace later.
Boundary statement: if you want global sharing, do not provide it again in the lazy-loaded module; if you want module isolation, write it plainly on that module instead of relying on accidentally getting the wrong instance.
FAQ
Are Angular services singletons by default?
A service with providedIn: 'root' is usually a singleton within the root injector; but if a lazy-loaded module provides it again, it becomes a different instance in that module's scope.
How do I tell whether I am hitting a multiple-instance problem?
Log an instance marker in the service constructor, inject the service from the root component and from the lazy-loaded module, and compare — different markers basically confirm multiple instances, then check where providers is declared.
Is deleting providers from the lazy-loaded module always the fix?
In most cases yes; if the service needs module-level initialization configuration, that configuration generally stays provided on the root side, and the lazy-loaded module only imports the module without providing again — refer to the official docs for specifics.
Do component-level providers also create new instances?
Yes. A component-level provided service is valid only within that component and its child components and is destroyed when you leave the component tree; it suits local state, not data that must be shared across pages like login state.
If there is only one service instance but the value is still stale, should I check the provider location first?
Not necessarily. That case is usually a read-timing or subscription issue: the instance updated but the component did not re-read the value, so start by checking where the value is read and where the subscription lives.
Action guide: first search the whole project for where the service is provided, and confirm whether it is root-provided or module-provided; if it needs global sharing, keep the root provider and remove the duplicate; if it needs isolation, write the module boundary clearly. Applicability boundary: this reasoning fits regular Angular projects; if a state management library is already in place, still make sure the service provision level and the state source stay consistent, so the two sets of state do not drift apart.
-
It's 2026—why won't a Vue 3 child component update after I destructure props when the parent changes a value?
Date: Sep 19, 2026 Read: 11
-
In 2026, why does a filtered list link show all the data when a colleague opens it?
Date: Sep 18, 2026 Read: 12
-
It's 2026 and a page still fires four APIs at once—one fails and the whole page goes blank. Is that a bug?
Date: Sep 17, 2026 Read: 16
-
z-index Is Still Covered After Going Very High: Does Increasing It Further Help in 2026?
Date: Sep 16, 2026 Read: 24
-
It’s 2026. Everyone nodded in requirements review—why are empty states and loading states still being filled in during integration?
Date: Sep 14, 2026 Read: 25




