16
votes

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?

2
I encounter many of the same questions when I first started working with redux/ngrxStore. I took heavy inspiration from the ngrx example app. github.com/ngrx/example-app If you look at the way they compose their state and retrieve values it can help. With Redux it is encouraged for you to normalize your states similar to database tables. My second hurdle was learning how to effectively use observables and rxjs. When done right you can really simplify your code and make it very readable (assuming you understand the rxjs operators) and your app very fast and testable.wiredprogrammer
These react/redux examples ported over to use ngrx/store might help some too. github.com/btroncone/ngrx-exampleswiredprogrammer
They essentially build indexes against the state. const newBookEntities = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => { return Object.assign(entities, { [book.id]: book }); }, {});wiredprogrammer
And then use that id to get directly to the object. state$ .select(s => s.entities[id]);wiredprogrammer
Did this help at all?wiredprogrammer

2 Answers

14
votes

For every action that is called on a reducer, the new state is returned.

From the example code in the question, 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());
 }
8
votes

Okay I'll give it a shot at explaining a way to set this up and hopefully do it in a way you and others can understand.

So if you are storing a array of objects and need to get access to a row by a certain id or key then the way the ngrx example app shows you can do this is to create a object containing your object which will use (in the case of their book app) the book's id as the property key. You can set any string as a property name. So say you have your state with a property called "entities" whose value is a blank object. You can then take your array and create properties on the "entities" Object.

export interface BooksState {
  entities: { [id: string]: Book };
};

Lets start with just one row in the array of book objects. To add this row to the "entities" object you do it like this.

 reducerState.entities[book.id] = book;

The above will make the book's id a property on the "entities" object.
If you were to inspect it in the console or debug tools it might look something like this after its creation.

 reducerState.entities.15: { id: 15, name: "To Kill a Mockingbird" }

The ngrx example app operates on the full array to add it to the state using the reduce operator on the javascript array.

   const newBookEntities 
              = newBooks.reduce(
                  (entities: { [id: string]: Book }, book: Book) => 
                           { 
                              return Object.assign(entities, { [book.id]: book }); 
                           }, {});

And then create special functions to get parts of this state that can be composed together later to get the specific rows you need

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


export function getBookEntities() {
  return (state$: Observable<BooksState>) => state$
   .select(s => s.entities);
};
export function getBooks(bookIds: string[]) {
  return (state$: Observable<BooksState>) => state$
              .let(getBookEntities())
              .map(entities => bookIds.map(id => entities[id]));
   }

And they compose these in the index.ts barrel in the example

import { compose } from '@ngrx/core/compose';


export function getBooks(bookIds: string[]) {
   return compose(fromBooks.getBooks(bookIds), getBooksState());
}

This function will chain the calls together going from right to left. First calling getBooksState to get the reducer state and then calling fromBooks.getBooks and passing in the bookid's you want and the state from the previous "getBooksState" and allowing you to do the mapping to the selected states you need.

In your component you can import this function and use it to get just the books you want.

myBooks$: Observable<Books>  =  this.store.let(getBooks(bookIds)));

This returned observable is updated every time the store is updated.

Added: So the operator "let" on the store object passes in the current observable holding the store's state object to the function that is returned by the "getBooks" function.

Lets look closer at that.

export function getBooks(bookIds: string[]) {
   return compose(fromBooks.getBooks(bookIds), getBooksState());
}

Two functions are passed into the compose function here -- fromBooks.getBooks(bookIds) and getBooksState(). The compose function allows a value to be passed into the first function on the right and have that functions results passed into the function on the left and so on and so on. So in this case we are going to take the results of the function returned by "getBooksState" and pass it into the function returned by the function "fromBooks.getBooks(bookIds)" and then ultimately return that result.

These two functions take in a Observable. In the case of the function returned by getBooksState() it takes in as a parameter the store state observable object from which it will map/select (map and select are aliases for each other) to the slice of the store we care about and return the new mapped observable. That mapped observable will be passed into the function returned from the fromBooks.getBooks(bookIds) function. This function expects the state slice observable. This function does the new mapping by pulling out only the entities we care about. This new observable is what is ultimately returned by the "store.let" call.

If you need more clarification then please ask questions and I'll do my best to clarify.

https://github.com/ngrx/example-app