ngrx / store

RxJS powered state management for Angular applications, inspired by Redux
MIT License
3.9k stars 311 forks source link

Type Safety on Reducer Re-Use #440

Closed Renader closed 7 years ago

Renader commented 7 years ago

Recently I struggled while slicing my store logic to a re-useable pattern.

The given scenario: I have orders and invoices - both share a table where the positions are listed. While order and invoice differ heavily, positions are the same.

Store may look like this (not normalized to simplify example):

{
  'invoices': {
    ...
    positions: [...]
  },
  'orders': {
    ...
    positions: [...
  }
};

In a redux world I would use a factory that takes a scope string variable and returns the reducer with scoped type in the switch case. This does work with ngrx/store as well, but I would use my type safety since the discriminated union types aren't able to interfere the actions type any longer.

Scoped Reducer Example: (types are lost)


export function positionReducerFactory(scope: string) {
  return function positionReducer(state: MyState = MyInitState, action: MyActions): MyState {
    switch (action.type) {
      case scope + LOAD_SUCCESS:
        return loadSuccessFn(action);
      case scope + LOAD_FAILURE:
        return loadFailureFn(state, action);
      ...
    }
  }
}

Coming to my question: Does @ngrx/store currently provides any utility for that that I'm missing? Should there be one? Or (and I know this belongs more to SO) is there a TypeScript way to resolve my issue?

joanllenas commented 7 years ago

This is how I solved this issue:

export function positionReducerFactory(scope: string) {
    const initialState: MyState = {
    };
    function positionReducer(state: MyState, action: MyActions): MyState {
        switch (action.type) {
            case LOAD_SUCCESS:
                return loadSuccessFn(action);
            case LOAD_FAILURE:
                return loadFailureFn(state, action);
            default
                return state;
        }
    }
    return (state: MyState = initialState, action: MyActions): MyState => {
        if (action.payload) {
            switch (action.payload.scope) {
                case scope:
                    return positionReducer(state, action);
                default:
                    return state;
            }
        } else {
            return state;
        }
    };
};

You have to define which scope you are targeting on each the action payload, so the meta reducer knows which reducer instance to use.

Renader commented 7 years ago

Thank you @joanllenas, very elegant!