Mike
Mike

Reputation: 309

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).

Upvotes: 2

Views: 3098

Answers (2)

Mike
Mike

Reputation: 309

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));
    }
}

Upvotes: 2

Kevin
Kevin

Reputation: 847

I would recommend rethinking your way of doing this and use the same reducer and make another switch case.

Unrelated to that, the newer version of AOT doesn't like using '=>' to create your reducers. Instead use

export function SearchReducer (state : State = initialState, { type, payload }){
   switch (type) {
      //cases...
   }
}

and you won't have to use combineReducers, you can just build out your reducer object

let reducers = {
   search: SearchReducer
}

Saying your state is of the interface State type lets you take advantage of that typing.

Upvotes: 0

Related Questions