Sometimes there are cases when you need to load static data from the server, such as the application configuration object. Since the data is static, it makes sense to want synchronous access to it, so you don't have to subscribe to an Observable every time you want to read a value in the configuration.
One of the standard ways is to use a Router Resolver, which allows you to load data from the server when the route is loaded, thus giving all child components synchronous access to the loaded data.
But what if you need to provide global access throughout the entire application, so that even services with providedIn: 'root'
can have synchronous access to the data?
In this article, we will explore a simple way to achieve the desired result.
Creating an initializer function
This issue can be easily addressed by creating an initialization function that will create the necessary providers for your config.
import { APP_INITIALIZER, InjectionToken, Provider } from '@angular/core';
export function initializeToken<T>(
token: T | InjectionToken<T>,
initFn: () => Promise<T>
): Provider[] {
let data: T | undefined = undefined;
return [
{
provide: APP_INITIALIZER,
multi: true,
useFactory: () => async () => (data = await initFn()),
},
{
provide: token,
useFactory: () => {
return data;
},
},
];
}
- In this function, we first create an
APP_INITIALIZER
provider that will load data using the provided function and save it in thedata
variable. - After that, we create a second provider that will register the obtained data using the provided token and the
useFactory
function.
The key point is that Angular calls the useFactory
function only when one of the components injects its token. Before that, your token will not be initialized.
Therefore, when creating your Angular application, Angular will first inject all APP_INITIALIZER
and wait for the completion of their asynchronous functions before starting the application. Only then will the application be launched. As a result, the second provider will be used only after the application starts and will already have the asynchronously loaded value.
Usage
Now we can use the created function as follows:
export abstract class MyConfig {
userId!: number;
id!: number;
title!: string;
completed!: boolean;
}
bootstrapApplication(App, {
providers: [
initializeToken(MyConfig, () =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) =>
response.json()
)
),
],
});
Now, if you try to inject MyConfig
into AppComponent
, you will be able to retrieve its value synchronously.
@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
template: `
Config: {{ config | json }}
`,
})
export class App {
config = inject(MyConfig);
}
Pitfalls
This solution may not work if you try to inject the config into a Router Guard with the initialNavigation: 'enabledBlocking'
parameter enabled. In this case, the router won't wait for the completion of the application initialization, and as a result, the data for your token may not have enough time to load. More information.
Conclusion
This approach to loading static data can help simplify many places in your application and eliminate Observables where they are not needed.
I tried to make the example as straightforward as possible, but you can also extend the initialization function to allow specifying deps
, thus having access to other tokens and services, such as HttpClient
.
I hope this approach proves useful to you.
Top comments (0)