问题
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:
feature modules should be imported in isolation from each other, as per consumer app's requirements.
multiple instances of the same feature should coexist inside the same parent (e.g. separate tabs with individual contexts)
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!
回答1:
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
来源:https://stackoverflow.com/questions/49537571/independent-instances-of-the-same-ngrx-feature-module