NGRX Component-store working in an Angular lazy-loaded module

364 views Asked by At

I have an Angular 16 app, where I lazy load modules in app.routes file, as shown below:

export const APP_ROUTES: Routes = [
  {
    loadChildren: () => import('./modules/users/users.routes').then(m => m.USERS_ROUTES),
    path: 'users',
  },
  {
    loadChildren: () => import('./modules/settings/settings.routes').then(m => m.SETTINGS_ROUTES),
    path: 'settings',
  },
]

File users.routes with routing for users module looks like this:

export const USERS_ROUTES: Route[] = [
  {
    path: '',
    providers: [UsersHttpRepository, UsersComponentStore],
    resolve: [() => inject(UsersComponentStore).loadUsers$()],
    children: [
      {
        path: '',
        component: UsersListPage,
      },
      {
        path: ':userId',
        component: UserDetailsPage,
      },
    ],
  },
];

Architecture of my app is very modular, where certain parts of it (/users, /settings etc) are separated modules, without overlapping or importing some common parts. I decided to go with NGRX component-store, because the data I keep in individual modules relates only to them, it doesn't go beyond them, so there's no need for it to be available globally. BUT I wanted one instance of component-store working across all the components in specific module (I would call it module-store), so the data can be shared between all the components.
I am aware, that this is not the exact use case for component store, which should be provided directly into a component, instead of a route.
The component store itself is very basic, part of here is shown below:

export const initialState: UsersComponentState = {
  users: []
  userDetails: null
};

@Injectable()
export class UsersComponentStore extends ComponentStore<UsersComponentState> {
  readonly users = this.selectSignal(state => state.users);
  readonly userDetails = this.selectSignal(state => state.userDetails);

  readonly loadUsers$ = this.effect<void>(_ =>
    _.pipe(
      switchMap(() =>
        this.repository.loadUsers().pipe(
          tapResponse({
            next: users =>
              this.patchState({users}),
            error: (error: HttpErrorResponse) => errorLogger(error)
          })
        )
      )
    )
  );

  constructor(private readonly repository: UsersHttpRepository) {
    super(initialState);
  }
}

What concerns me here, is that when user leaves the route, and navigates somewhere else outside users module, the ngOnDestroy hook is not executed, so the instance of this component-store is never destroyed. After coming back to the /users route, all the data is still available there. It is a different behavior from when component-store is provided directly into a component and destroy with it.
I understand that this is probably a correct behavior, because once route and module is lazy loaded it is never destroyed, so the providers that i put in routes array here:

export const USERS_ROUTES: Route[] = [
  {
    path: '',
    providers: [UsersHttpRepository, UsersComponentStore],

are also never destroyed.
My question is whether I should be worried about this? Ultimately, I want to have a component-store like this one in each of my module and I am concern about some memory leaks or unfinished subscriptions.

2

There are 2 answers

2
Volodymyr Usarskyy On

Whether you have memory leaks or unfinished subscriptions or not is completely up to your code. No one can tell you that just by your question here.

But what you can try is to use an empty shell/container component that will register UsersComponentStore in its providers, so all shell's children will get this instance. After shell component is destroyed, its providers will be destroyed as well

UPDATE: My apologies. Actually, you don't even need an empty shell component. My suggestion should work with an empty route without a component as well.

0
Geo242 On

As the documentation mentions for ComponentStore, ComponentStore should be used to "manage local/component state". It was intended to be used in the providers array of an individual component, not in the route providers.

If you really need to prevent the route from activating before the users are loaded, I think what you are really looking for is the main Ngrx store with actions and a reducer. You could implement your example using a guard that triggers an action to load the users and then waits for a selector the indicates whether loading the users is done or not.

If you don't need to prevent the route from activating, you could just use your ComponentStore in your users component providers array. Then you could just show a loading indicator on that page while the users are loading.