Search code examples
angularngrxngrx-store

Selectors for multiple instance of NGRX reducers


I have a reducer that use for search and realized that it needs to be used for multiple un-related search components. So, looking through the Redux documentation I found the concept of higher order reducers (http://redux.js.org/docs/recipes/reducers/ReusingReducerLogic.html#customizing-behavior-with-higher-order-reducers) (meta reducers in ngrx) and used that to create 2 'instances' of my search reducer. I then found in the same documentation that this will appear to work with selectors but actually has an issue with the memoization (http://redux.js.org/docs/recipes/ComputingDerivedData.html#accessing-react-props-in-selectors). That article references a function called 'mapStateToProps' which seems to be React specific way of connecting the store data to components (if I understand it correctly...).

Is there an equivalent in ngrx or is there another way of creating these selectors to work with the different instances of the reducers?

Below is a mildly contrived example based on the ngrx example app of what I am trying to accomplish:

reducers/searchReducer.ts:

export interface State {
  ids: string[];
  loading: boolean;
  query: string;
};

const initialState: State = {
  ids: [],
  loading: false,
  query: ''
};

export const createSearchReducer = (instanceName: string) => {
  return (state = initialState, action: actions.Actions): State => {
    const {name} = action; // Use this name to differentiate instances when dispatching an action.
    if(name !== instanceName) return state;

    switch (action.type) { 
      //...
    }
  }
}

reducers/index.ts:

export interface State {
  search: fromSearch.State;
}

const reducers = {
  search: combineReducers({
    books: searchReducer.createReducer('books'),
    magazines: searchReducer.createReducer('magazines')
  }),
}


export const getSearchState = (state: State) => state.search;

// (1)
export const getSearchIds = createSelector(getSearchState, fromSearch.getIds);

I believe the getSearchIds selector above needs the ability somehow to specify which instance of the search Reducer it is accessing. (Strangely, in my code it seems to work but I am not sure how it knows which to select from and I assume it has the memoization issue discussed in the Redux documentation).


Solution

  • While Kevin's answer makes sense for the contrived code example I gave, there are definitely maintenance issues if each reducer 'instance' has many properties or if you need many 'instances'. In those cases you would wind up with many quasi-duplicate properties on a single reducer (ex. 'bookIds', 'magazineIds', 'dvdIds', 'microficheIds', etc.).

    With that in mind, I went back to the Redux documentation and followed it to the FAQ for selectors, specifically How Do I create a Selector That Takes an Argument.

    From that information, I put this together:

    reducers/index.ts:

    export const getBookSearchState = (state: State) => state.search;
    export const getMagazineSearchState = (state: State) => state.search;
    
    // A function to allow the developer to choose the instance of search reducer to target in their selector. 
    export const chooseSearchInstance = (instance: string): ((state: State) => searchReducer.State) => {
        switch(instance) {
            case 'books': {
                return getBookSearchState;
            }
            case 'magazines': {
                return getMagazineSearchState;
            }
        }
    }
    
    // Determines the instance based on the param and returns the selector function.
    export const getSearchIds = (instance: string) => {
        const searchState = chooseSearchInstance(instance);
        return createSelector(searchState, state => state.ids);
    }
    

    In some component where you know the reducer you want to use:

    //...
    class SearchComponent {
        @Input()
        searchType: string = 'books'; 
        ids: Observable<number>;
    
        constructor(private store: Store<fromRoot.State>) {    
            this.store.select(fromRoot.getSearchIds(searchType));
        }
    }