DEV Community

Alex Skoropad
Alex Skoropad

Posted on

Synchronous access to global async data in Angular

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;
      },
    },
  ];
}
Enter fullscreen mode Exit fullscreen mode
  • In this function, we first create an APP_INITIALIZER provider that will load data using the provided function and save it in the data 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()
      )
    ),
  ],
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)