We're developing SPA using Angular 7 in a combination with NGRX. We have migrated to NGRX a few months ago to use mainly for authentication and session data. After that, we have started moving other functionalities as well. I will describe the problem and what solutions we have in our heads and their drawbacks. I would be very thankful to hear how you solved this problem(if you had) or ideas on how to solve having problems.
Motivation to use Redux(NGRX)
- We had data that we needed to share between multiple components. Sharing through properties was not possible since we had components separated by Angular router.
- Reduce number of requests by reusing loaded data like Auth/Session
- Performance - Angular performs considerably faster when the component is using
ChangeDetectionStrategy.OnPush
Business problem
Inside our application, we have 6 modules. Each module has at least 10 features(pages). We have just few common details shared between them.
First steps with Redux Store
Since our main problem was #1 and #2(described above), we started with following store structure:
{
"authentication": {
"isLoading": false,
"error": {},
"token": "auth-token"
},
"session": {
"userDetailsIsLoading": false,
"userDetailsData": {},
"userDetailsError": {}
}
}
So far, so good. It was simply like all Redux Tutorials with increase/decrease/reset counter.
Migrating modules features
Here the fun began since we have a lot of features inside app (6 modules * 10 features = 60+).
The first decision was to use reducer per feature
and not reducer per entity
because we have pages with the same type of entity, but fetched with different filters. We're filtering data on the server side, because of a big amount of data.
Ex: Recent 10 posts, Top 10 posts - both on the same page
So, we have extended our store with more reducers:
{
"authentication": {},
"session": {},
"blogs-list": {},
"blogs-add": {},
"administration-users": {},
"administration-add-user": {}
}
Problems
Action type collision
Yes, we had first type collision as we were using format [Feature] Action Description
.
To solve it, we have decided to add use a namespace [Module][Feature] Action Description
.
Big store
That's the biggest problem. The entire store becomes very big. It's not a problem when the store is clean, but after a few navigations, the store contains a lot of unneeded data.
Solutions which we considered
Lazy loading
Angular allows to lazy-load so-called NGRX Features
. This means we can start with just shared data(auth, session) and once a module is opened, all reducers are added to the store. This solution solves the problem partially. If you navigate to all modules, the store becomes big again.
Switching to entity reducers
This looks easy in theory, but hard in practice. As I mentioned, we have a kind of dashboard where we display the same entities but fetched with different filters + pagination. In the head I have an idea to have different reducers for these cases, like:
{
"latest-entities": {
"isLoading": bool,
"data": {},
"error": {},
},
"most-popular-entities": {
"isLoading": bool,
"data": {},
"error": {},
}
}
but there we have another drawback. We used to keep filters in feature-store and once we change filters, effects are triggering data reload. Switching to this architecture, we move the responsibility of data reloads to the containers
. Container
will be responsible for dispatching actions to both stores latest-entities
and most-popular-entities
. This solution will also get bigger and bigger. The problem couldn't be fully solved by reorganizing reducers and store.
Don't move features to Redux but keep them in containers
We can keep feature logic in containers
and use redux only for cross-module data like a session, auth, and notifications. This is also a solution but has its drawbacks. I would like to keep the application consistent and not to have business logic in both containers and store.
Reset stores
To keep the store smaller, we can create an action that will reset the store to the initial state which will be dispatched once the feature is not used. Here is an example:
public ngOnDestroy(): void {
this.store$.dispatch(new BlogStoreActions.DestroyAction());
}