reach4thelasers reach4thelasers - 3 months ago 131
TypeScript Question

get single item from ngrx/store

I've written the following reducer to store the state items in my Angular 2 app. The Items are price offers for Financial Instruments (e.g. stocks/currencies).

My Reducer Implementation is as follows:

export const offersStore = (state = new Array<Offer>(), action:Action) => {
switch(action.type){

case "Insert":
return [
...state, action.payload
];
case "Update":
return state.map(offer => {
if(offer.Instrument === action.payload.Instrument)
{
return Object.assign({}, action.payload);
}
else return offer;
});
case "Delete":
return state.filter(offer => offer.Instrument !== action.payload )
default:
return state;
}


}

I managed to get Inserts, Updates and Deletes working - although it wasn't easy. I find Redux to be something of a paradigm shift away from how I've been coding for years.

I've got an Instrument Component/Page on my App - which shows all available information for one specific Instrument, indicated by InstrumentId e.g. "EUR/USD" (stored in the payload.Instrument property).

My problem is, I'm not sure how to efficiently search for a specific instrument and grab it out of the store. Not only this, but I also want the instrument I fetch to be updated if the Instrument in the store is updated as they are frequently via websocket push from the server. So I really need to search the store for a specific instrument, and return it as an Observable, that will continue to update the View Component based on new data that gets pushed to the store.

How can I achieve this?

Answer

What you're not getting is that for every action that is called on a reducer, the new state is returned.

From your example code in the question, your state is just a list of instruments.
There's no index, so the only way to check if an instrument is in the list is to search the whole list.

But what if your state was a dictionary? Furthermore, what if you kept a list of indexes seperate to the dictionary?

your state type is this:

export interface OfferState {
  ids: string[];
  entities: { [id: string]: IOffer };
};

Any time an action is executed, the new state is returned. It is an important concept in Redux, because state can never be mutated directly. You're actually best strictly enforcing this when you compose your reducer: (say you've got you "offers reducer" and another reducer, you combine them to one with compose:

> export default compose(storeFreeze, combineReducers) ({   oether:
> otherReducer,   offers: offersReducer });

Its easy to do things wrong in Redux - but using storeFreeze will throw up an error if you try to mutate the state directly. The point is that actions change state, and make a new state. They don't change the existing state - it lets us undo/redo... etc.

Using your example above I would use this as my Offer's reducer:

export interface OfferState {
  ids: string[];
  entities: { [id: string]: IOffer };
};

export default function(state = initialState, action: Action): OfferState {
    switch(action.type){
        case OfferActions.INSERT:
        const offer : IOffer = action.payload;
        return {
            ids: [ ...state.ids, action.payload.Instrument ],
            entities: Object.assign({}, state.entities, { [action.payload.Instrument]: action.payload})
        };
        case OfferActions.UPDATE:
            return {
                ids: [...state.ids],
                entities: Object.assign({}, state.entities,  { [action.payload.Instrument]: action.payload})
            }

        default:
            return state;
    }
}

note that changes are made to a temporary state via object.assign (deep copy) and then the new state is returned.

The other answer to the question was a bit confusing. It went into the detail of how to combine different reducers, but it didn't make much sense to me.

in your reducers/index.ts you should have a type:

export interface AppState {
  otherReducer: OtherReducer;
  offers: fromOffersReducer.OfferState;
}

inside this index.ts, you should have functions that get the reducers:

export function getOfferState() {
  return (state$: Observable<AppState>) => state$
    .select(s => s.offers);
}

export function getOtherReducer() {
  return (state$ : Observable<AppState>) => state$
    .select(s => s.otherReducer)
}

inside our offerReducer and our otherReducer, we define functions that can query the data we need. These are anonymous functions, that are not linked to anything at present, but we will link them later (to the getReducerFunctions).

examples of these functions:

export function getOfferEntities() {
  return (state$: Observable<OfferState>) => state$
    .select(s => s.entities);
};

export function getOffer(id: string) {
  return (state$: Observable<OfferState>) => state$
    .select(s => s.entities[id]);
}

this does nothing. unless we apply it to some useful data (e.g. the offersRedeucer) that we made ealier, and we combine the two like this:

import offersReducer, * as fromOffersReducer from './OffersReducer';

 export function getOfferEntities() {
   return compose(fromOffersReducer.getOfferEntities(), getOfferState());
 }

  export function getOffer(instrument:string) {
   return compose(fromOffersReducer.getOffer(instrument), getOfferState());
 }

I've tried to anticipate the next problem you will face (a second reducer) as well as answering the question of "how do you get one item?" as I know you're going to stumble onto that issue pretty much immediately. But don't let that confuse you with your original query.... The trick to finding an item, is to maintaining a state which is a map of items (a dictionary) - never mutating that state and always returning a new state. Follow that guidance and your problem is solved. The next problem you will face is multiple reducers, but read again above and it should make sense.

I disagree with the previous answer about stackoverflow questions - they are whatever you need them to be. And if you need a tutorial, then they can be a tutorial. And I don't think that's a bad thing - particularly in niche subjects like RXJS where little advice and documentation is available.

Thanks for putting a bounty on this question - I hope that I have given you a better answer. Any further questions - just yell. I am happy to help. You will not get a "I'm reluctant to answer any more of your questions" from me... I am here to help - not to insult.