9

Background:

Team is building a large REST-based web application using Angular and the @ngrx library for state management.

We wish to model entities from the server as TypeScript classes. These may be: accounts, users etc

This achieves:

  • Loose coupling to the API; if the response changes, only the model must change
  • Encapsulating basic functionality e.g. string concatenation of first and last name to make fullName

The uncertainty lies in when, during the application's timeline, to initialise the model, calling: new Account(accountResponse).

Conventional logic suggests to do this as early as possible, in a service along side the logic to retrieve the accounts (be it from a cache, server response, etc).

this.apiService.fetch(AccountService.URL)
      .map(accounts => accounts.map((a: AccountResponse) => new Account(a)));

This method is invoked by an ngrx effect, then following a successful response, the Account objects are added to the store by the reducer.

This works, however... ngrx / redux "best practice" states only plain objects and primitives should be kept in the store, for ease of serialisation among other reasons.

To adhere to this advice, initialising the Account object must happen much further down the line. Either in individual components, in a state selector, or generally wherever an account is used.

This doesn't make sense to me, as the raw account response objects are being passed around the application, somewhat defeating the point of wrapping them in a model in the first place.

The application is structurally similar to @ngrx/example book app which, given its simplicity, does not wrap the server responses in model objects.


Questions:

  • What are the detrimental effects of keeping initialised classes in the store (besides serialisability)?

  • If only plain objects are to be kept in the store, where in the flow of data through the app are model classes best initialised?

kyranjamie
  • 995
  • 14
  • 26
  • Opted to initialised model classes in the state selector & ensure all classes implement at `serialize` fn returning plain object. Seemed like best option as: 1) state is only ever read from a selector, thus we always interactive with models, and: 2) whenever state is updated the serialize fn I invoked from reducer – kyranjamie May 31 '17 at 09:19
  • 1. is your model direct 1-1 mapping to server response json? 2. do you have something special in `new Account(response)` constructor other than just assigning model properties to response json properties? – dee zg Jul 29 '17 at 15:26
  • One consequence worth mentioning is how selectors behave when they are responsible for initialising models. Internally, `select` uses the `distinctUntilChanged()` operator. This performs a reference equality check to not emit unchanged values. Initialising a new object with `new` however, will always make an object appear different, regardless of the data is contains has changed. So you'll notice more emits than expected. – kyranjamie May 14 '18 at 09:14
  • @kyranjamie - I just started with `ngrx`. And I'm asking myself the same question. I was wondering how did you end up solving it. Thanks a lot in advance! – stevo Oct 28 '19 at 07:47
  • 2
    @stevo We took the concession of not initialising them in the data services. Instead, ensuring the plain response object is never used, or read from, outside the data service or store. Then, created a wrapper service to read from the store and initalise the object. Something like: `$users = this.store.pipe(select(selectUsers), map(user => new User(user)))` – kyranjamie Oct 28 '19 at 20:33
  • Hi @kyranjamie, do you still see that as the proper way of doing it? Creating a service to request the store sounds painful to me. Does that service also serializes the object when you save the state back to the store? Did you use something like class-transformer to do that and how did you type check your data in the store? – Eric Jeker Nov 03 '20 at 14:56
  • It wasn't perfect, but it did work. In this set up, as I recall, each domain object implemented a `.serialize()` method, used when writing back to the store. – kyranjamie Nov 05 '20 at 08:05
  • We didn't use `class-transformer`. Using services created an extra abstraction, but given we had a small set of domain objects, this wasn't much overhead. A suitable alternative might be to just use (typed) plain objects, and infer data with helper functions, avoiding classes all together. – kyranjamie Nov 05 '20 at 08:15

3 Answers3

3

The easiest way to work with ngrx is to see it like a database, not just a javascript shared object cache. You would not store javascript helpers in the database, same for ngrx.

You can either stop using model functions and instead use utility methods when needed, or you can wrap the selector results using an observable operator. Something like state.pipe(select(mySelector), myWrapFunction) though you will end up re-creating your wrapper every time.

You might want to look at the viewmodel design pattern (e.g. [MVVM] (https://en.m.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel)) to see approaches similar to what you are trying to do.

Christian Rondeau
  • 4,143
  • 5
  • 28
  • 46
0

Check this example on how to initialize the state of a feature in the ngrx store. I hope this is what you are looking for.

RV.
  • 2,781
  • 3
  • 29
  • 46
  • Thanks, however this example only uses plain objects, so isn't relevant for this question. – kyranjamie Oct 09 '18 at 10:51
  • You may want to check https://github.com/johnpapa/angular-ngrx-data if you are planning to map entities in ngrx. – RV. Oct 09 '18 at 22:43
  • Also state should be serializable. Adding classes into state makes it unserializable. If you plan never to serialize / transfer state from the server or store some of the data on browser localstorage / indexedDB or elsewhere then it's somewhat ok. – MTJ Aug 07 '19 at 08:17
0

@kyranjamie I'm trying to understand how to do in best-practice way. Did I understand you correct? You store a plain object in a state. When a component needs the object from the state it uses a service, for example: HomeService - to get HomeModel object instead of a plain object

@Injectable({
  providedIn: 'root'
})
export class HomeService {

  constructor() {
  }

  public getSelectedHome(homeObject: HomeJSON) {
    return Object.assign(new HomeModel(), homeObject);
  }
}

A component needs to subscribe to the state. It's subscribed to a change in the state, but instead of using directly the state plain object, it uses HomeService to make the plain object into a class object(HomeModel).

ngOnInit() {
    this.store.pipe(select(getSelectedHome)).subscribe(
      home => {
        this.home = this.homeService.getSelectedHome(home);
      }
    );
  }

Is this how you do it?

Olga Zhe
  • 163
  • 7
  • or do it directly in selectors? For example, in **getSelectedHome** I can use **HomeService** to "translate" a plain object from the state into a class object(HomeModel). What is the best approach? – Olga Zhe Mar 11 '20 at 09:21
  • Not quite, we ended up following the facade pattern, with services specifically for reading from the store. My last comment on the question kinda explains; we'd create an observable from the store, as your `select` does above, and then `map` that stream to initialise the models. No `subscribe` in the service though, we'd do this in the components themselves using the `async` pipe. – kyranjamie Mar 25 '20 at 13:49