Search code examples
angularngrx

Lazy Feature Modules architecture migration to NGRX: Angular 8


Hey guys currently i am working in an application with Lazy features modules architecture implemented. Below you can see how the project is structured.

enter image description here

Since the application is keep on growing we decided to migrate it to Ngrx.

As is a new pattern for me i am searching for migration guidelines but i can only find ngrx guidelines when creating a project from scratch.

Could you please give me some hints, guidelines,where should i be careful , and possibly some steps summary?

Thanks you.


Solution

  • guidelines

    It's possible to lazy load your store but this has caused me more issues than benefits. For example getting a selected project based on the router url and loaded project entities which mixes two feature stores. The following article has provided me a nice way to split up the store whilst allow any part of the application access to store data:

    https://itnext.io/ngrx-best-practices-for-enterprise-angular-applications-6f00bcdf36d7

    For posterity the application structure looks like:

    ├── app
     │ ├── app-routing.module.ts
     │ ├── app.component.css
     │ ├── app.component.html
     │ ├── app.component.ts
     │ ├── app.module.ts
     │ ├── components
     │ ├── containers
     │ │    └── my-feature
     │ │         ├── my-feature.component.css
     │ │         ├── my-feature.component.html
     │ │         └── my-feature.component.ts
     │ ├── models
     │ │    ├── index.ts
     │ │    └── my-model.ts
     │ │    └── user.ts
     │ ├── root-store
     │ │    ├── index.ts
     │ │    ├── root-store.module.ts
     │ │    ├── selectors.ts
     │ │    ├── state.ts
     │ │    └── my-feature-store
     │ │    |    ├── actions.ts
     │ │    |    ├── effects.ts
     │ │    |    ├── index.ts
     │ │    |    ├── reducer.ts
     │ │    |    ├── selectors.ts
     │ │    |    ├── state.ts
     │ │    |    └── my-feature-store.module.ts
     │ │    └── my-other-feature-store
     │ │         ├── actions.ts
     │ │         ├── effects.ts
     │ │         ├── index.ts
     │ │         ├── reducer.ts
     │ │         ├── selectors.ts
     │ │         ├── state.ts
     │ │         └── my-other-feature-store.module.ts
     │ └── services
     │      └── data.service.ts
     ├── assets
     ├── browserslist
     ├── environments
     │ ├── environment.prod.ts
     │ └── environment.ts
     ├── index.html
     ├── main.ts
     ├── polyfills.ts
     ├── styles.css
     ├── test.ts
     ├── tsconfig.app.json
     ├── tsconfig.spec.json
     └── tslint.json
    

    where should i be careful

    Make sure your reducer returns the state unamended for undefined actions. You can test for this. There's no excuse not to test your reducers. These are pure functions and easy to test.

    import * as fromProjects from './project.reducer'
    import * as fromProjectState from './project.state'
    
    describe('ProjectReducer', () => {
      describe('undefined action', () => {
        it('should return the default state', () => {
          const { initialState } = fromProjectState
          const action = {} as any
          const state = fromProjects.reducer(initialState, action)
    
          expect(state).toBe(initialState)
        })
      })
    })
    

    Take the time to ensure your actions types are correct - the errors are hard to debug. Due to boilerpate you'll likely copy and paste alot of code here. Again this can be tested.

    describe('LoadProjectsFail', () => {
      it('should create an action', () => {
        const payload = { message: 'Load Error ' }
        const action = new fromProjects.LoadProjectsFail(payload)
        expect({ ...action }).toEqual({
          type: fromProjects.LOAD_PROJECTS_FAIL,
          payload,
        })
      })
    })
    

    Keep to the NgRx documentation - there's been a few changes and tutorials are normally at least one version behind. e.g.

    this.store.pipe(select(projectSelectors.getProjectsLoading))
    // versus
    this.store.select(projectSelectors.getProjectsLoading)
    

    steps summary

    Pretty much the same as the link but a different order:

    Root Store

    1. Write root-store.module.ts
    import { CommonModule } from '@angular/common';
    import { NgModule } from '@angular/core';
    import { EffectsModule } from '@ngrx/effects';
    import { StoreModule } from '@ngrx/store';
    import { StoreDevtoolsModule } from '@ngrx/store-devtools'
    import { environment } from 'src/environments/environment'
    // import { MyFeatureStoreModule } from './my-feature-store/';
    // import { MyOtherFeatureStoreModule } from './my-other-feature-store/';
    
    @NgModule({
      imports: [
        CommonModule,
        // MyFeatureStoreModule,
        // MyOtherFeatureStoreModule,
        StoreModule.forRoot({}),
        EffectsModule.forRoot([])
        // Store devtools
        !environment.production
          ? StoreDevtoolsModule.instrument({
              name: 'My App',
            })
          : [],
      ],
      declarations: []
    })
    export class RootStoreModule {}
    

    Add the following files which will start of pretty much empty:

    • index.ts
    
        import { RootStoreModule } from './root-store.module'
        import * as RootStoreState from './state'
        import * as RootStoreSelectors from './selectors'
        //export * from './employee'
        //export * from './project'
        //export * from './router'
        export { RootStoreState, RootStoreSelectors, RootStoreModule }
    
    
    • state.ts
    
        import { routerState } from './router'
        import { employeeState } from './employee'
        import { projectState } from './project'
    
        export interface State {
          router: routerState.State
          employees: employeeState.State
          projects: projectState.State
        }
    
    
    • selectors.ts
    
        import { createSelector } from '@ngrx/store'
        import { Project } from './project/project.model'
        import { routerSelectors } from './router'
        import { projectSelectors } from './project'
    
        export const getSelectedProject = createSelector(
          projectSelectors.getProjectsEntities,
          routerSelectors.getRouterState,
          (entities, router): Project => {
            return router.state && entities[router.state.params.id]
          }
        )
    
    
    1. Import RootStoreModule into app.module.ts

    Feature Store

    1. Define your Feature State
    2. Define Feature Actions
    3. Write your Feature Effects
    4. Write your Feature Reducer (Test first if possible)
    5. Write your Feature Module
    6. Write your index.ts, add to root index.ts
    7. Add Feature State to Root State
    8. Declare Feature Module as part of Root Store module.
    9. Define Feature Selectors
    10. Define any Root Selectors (mixes feature selectors)

    Stackblitz