Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Independent instances of the same NgRx feature module

I am working on an Angular 5 project using NgRx 5. So far I've implemented a skeleton app and a feature module called "Search" which handles its own state, actions and reducers in an encapsulated fashion (by using the forFeature syntax).

This module has one root component (search-container) which renders an entire tree of child components - together they make up the search UI and functionality, which has a complex state model and a good number of actions and reducers.

There are strong requirements saying that:

  1. feature modules should be imported in isolation from each other, as per consumer app's requirements.

  2. multiple instances of the same feature should coexist inside the same parent (e.g. separate tabs with individual contexts)

  3. instances shouldn't have a shared internal state but they should be able to react to the same changes in the global state.

So my question is:

How can I have multiple <search-container></search-container> together and make sure that they function independently? For example, I want to dispatch a search action within one instance of the widget and NOT see the same search results in all of the widgets.

Any suggestions are much appreciated. Thanks!

like image 634
Stefan Orzu Avatar asked Mar 28 '18 14:03

Stefan Orzu


People also ask

Can we have multiple stores in NgRx?

For reuse and separability we require (at least) two state stores that do not interact with each other. But we do need both stores to be active at the same time, and potentially accessed from the same components. Ngrx seems to be predicated on the assumption that there will only ever be one Store at once.

When should you not use NgRx?

When should you not use NgRx? Never use NgRx if your application is a small one with just a couple of domains or if you want to deliver something quickly. It comes with a lot of boilerplate code, so in some scenarios it will make your coding more difficult.

What is a Metareducer?

What is a meta-reducer? A meta-reducer is just a fancy name for higher order reducer (i.e., function). Because a reducer is just a function, we can implement higher order reducer — “meta reducer.” higher order function is a function that may receive a function as parameter or even return a function.

Is NgRx store persistent?

When you install the Redux DevTools addon in your browser while instrumenting your store with @ngrx/store-devtools you'll be able to persist the state and action history between page reloads. You can't really ask your users to install a browser extension.


1 Answers

I had a similar problem to yours and came up with the following way to solve it.

Reiterating your requirements just to make sure I understand them correctly:

  • You have one module "Search" with own components/state/reducer/actions etc.
  • You want to reuse that Module to have many search tabs, which all look and behave the same

Solution: Leverage meta data of actions

With actions, there is the concept of metadata. Basically, aside from the payload-Property, you also have a meta-property at the top level of your action object. This plays nicely with the concept of "have the same actions, but in different contexts". The metadata property would then be "id" (and more things, if you need them) to differentiate between the feature instances. You have one reducer inside your root state, define all actions once, and the metadata help the reducer/effects to know which "sub-state" is called.

The state looks like this:

export interface SearchStates {
  [searchStateId: string]: SearchState;
}

export interface SearchState {
  results: string;
}

An action looks like this:

export interface SearchMetadata {
  id: string;
}

export const search = (params: string, meta: SearchMetadata) => ({
  type: 'SEARCH',
  payload: params,
  meta
});

The reducer handles it like this:

export const searchReducer = (state: SearchStates = {}, action: any) => {
  switch (action.type) {
    case 'SEARCH':
      const id = action.meta.id;
      state = createStateIfDoesntExist(state, id);
      return {
        ...state,
        [id]: {
          ...state[id],
          results: action.payload
        }
      };
  }
  return state;
};

Your module provides the reducer and possible effects once for root, and for each feature (aka search) you provide a configuration with the metadata:

// provide this inside your root module
@NgModule({
  imports: [StoreModule.forFeature('searches', searchReducer)]
})
export class SearchModuleForRoot {}


// use forFeature to provide this to your search modules
@NgModule({
  // ...
  declarations: [SearchContainerComponent]
})
export class SearchModule {
  static forFeature(config: SearchMetadata): ModuleWithProviders {
    return {
      ngModule: SearchModule,
      providers: [{ provide: SEARCH_METADATA, useValue: config }]
    };
  }
}



@Component({
  // ...
})
export class SearchContainerComponent {

  constructor(@Inject(SEARCH_METADATA) private meta: SearchMetadata, private store: Store<any>) {}

  search(params: string) {
    this.store.dispatch(search(params, this.meta);
  }
}

If you want to hide the metadata complexity from your components, you can move that logic into a service and use that service in your components instead. There you can also define your selectors. Add the service to the providers inside forFeature.

@Injectable()
export class SearchService {
  private selectSearchState = (state: RootState) =>
    state.searches[this.meta.id] || initialState;
  private selectSearchResults = createSelector(
    this.selectSearchState,
    selectResults
  );

  constructor(
    @Inject(SEARCH_METADATA) private meta: SearchMetadata,
    private store: Store<RootState>
  ) {}

  getResults$() {
    return this.store.select(this.selectSearchResults);
  }

  search(params: string) {
    this.store.dispatch(search(params, this.meta));
  }
}

Usage inside your search tabs modules:

@NgModule({
  imports: [CommonModule, SearchModule.forFeature({ id: 'searchTab1' })],
  declarations: []
})
export class SearchTab1Module {}
// Now use <search-container></search-container> (once) where you need it

If you your search tabs all look exactly the same and have nothing custom, you could even change SearchModule to provide the searchContainer as a route:

export const routes: Route[] = [{path: "", component: SearchContainerComponent}];

@NgModule({
    imports: [
        RouterModule.forChild(routes)
    ]
    // rest stays the same
})
export class SearchModule {
 // ...
}


// and wire the tab to the root routes:

export const rootRoutes: Route[] = [
    // ...
    {path: "searchTab1", loadChildren: "./path/to/searchtab1.module#SearchTab1Module"}
]

Then, when you navigate to searchTab1, the SearchContainerComponent will be rendered.

...but I want to use multiple SearchContainerComponents inside a single module

You can apply the same pattern but on a component level:

Create metadata id randomly at startup of SearchService.
Provide SearchService inside SearchContainerComponent.
Don't forget to clean up the state when the service is destroyed.

@Injectable()
export class SearchService implements OnDestroy {
  private meta: SearchMetadata = {id: "search-" + Math.random()}
// ....
}


@Component({
  // ...
  providers: [SearchService]
})
export class SearchContainerComponent implements OnInit {
// ...
}

If you want the IDs to be deterministic, you have to hardcode them somewhere, then for example pass them as an input to SearchContainerComponent and then initialize the service with the metadata. This of course makes the code a little more complex.

Working example

Per module: https://stackblitz.com/edit/angular-rs3rt8

Per component: https://stackblitz.com/edit/angular-iepg5n

like image 141
dummdidumm Avatar answered Nov 16 '22 01:11

dummdidumm