How to prevent observable emitted value from being mutated - javascript

I'm subscribing to observable and unshift a new element into the resulting array which mutates data. How can I prevent data mutation on a service level by throwing exception if someone tries to mutate data instead of doing deep copy?
I have an angular service to get the list of states from API based on country code. It's a singleton service meaning same instance is shared between different modules and their components. I'm caching the response with ShareReplay(). When in my component I subscribe to the observable and mutate the result (by unshifting a new value to the array) the cached data gets mutated. Right now I'm doing deep copy on a result which is fine but I want my observable to throw exception if somebody tries to use the service and mutates the original value instead of doing deep copy. I also tried to make service return deep copy of the observable but that didn't work, guess lodash doesnt know how to deep copy observables, unless I ditch shareReplay(), implement my own ReplaySubject on service level and make next() method return deep copy of emitted value maybe ?
Service
#Injectable()
export class StatesService {
private states$: Observable<State[]> = null;
constructor(private _http: HttpService) {}
getStatesFromCountry(countryCode3: string, freshData: boolean = false): Observable<State[]> {
let code = Object.keys(CountryEnum)
.filter((key: any) => isNaN(key))
.find(key => key === countryCode3.toUpperCase());
if(!code) return Observable.throw("Country Code was not recognized. Make sure it exists in CountryEnum.");
let url: string = `api/states?country=${countryCode3}`;
if (freshData) return this.getStates(url);
return this.states$ ? this.states$ : this.states$ = this.getStates(url).shareReplay(1);
}
private getStates(url: string): Observable<State[]> {
return this._http.get(url)
.map(response => response)
.catch(error => Observable.throw(<ApiError>JSON.parse(error)));
}
}
Component
private loadStates(): Subscription {
const usaStates$ = this._statesService.getStatesFromCountry(CountryEnum[CountryEnum.USA]);
const canStates$ = this._statesService.getStatesFromCountry(CountryEnum[CountryEnum.CAN]);
return Observable.forkJoin(usaStates$, canStates$).subscribe(
([usaStates, canStates]) => {
this.statesList = _.cloneDeep(usaStates.concat(canStates));
//Here if I unshift without doing deep copy first, other
//components that are using this shared service will now
//receive a mutated array of states
this.statesList.unshift(<State>{id: null, code: 'All'});
},
error => { ApiError.onError(error, this._notificationService, this._loaderService); }
);
}

You should be able to use Object.freeze on the value (somewhere).
const test = [1,2,3,4];
Object.freeze(test);
test.unshift(0); // error will be thrown
Cannot add property 5, object is not extensible

Related

Extracting data from subscribe() method

I don't really know how to extract the values from the subscribe() method.
getMessages(): any {
this.gatewayMessagesState$.subscribe(data => data.gatewayMessages
.get(this.gatewayId)
?.list
.map(Message => Message.message));
}
gatewayMessagesState is an initial state that contains some data. gatewayMessages is a map with gatewayIds as keys and arrays of Message objects as values. Message has message field that's just a string. I would like to extract an array of messages for a given id. How can I do that?
What you probably want to do is to populate another Observable with the data so that you can access it elsewhere in your project without the need for calling the API more than once.
To do this, you create what is known as a Subject (in this case a BehaviorSubject) and you can populate that with data when your API call returns a response.
Then, in order to access this data elsewhere, you can create a "get" function to return the Subject (which is itself an Observable) whenever you need the data.
Here is an example:
my - data.service.ts
myData: BehaviorSubject < number > = new BehaviorSubject < number > (0);
callApi() {
this.dbService.get('apiUrl').subscribe(
(data) = > this.myData.next(data) // Assuming data is a 'number'
);
}
getMyData() {
return this.myData.asObservable();
}
Now to use this in a component:
this.myService.getMyData().subscribe(
(data) = > {
/* Use the value from myData observable freely */
}
);
Or you could rely on the Angular async pipe (which is a very convenient method for dealing with observables in your code).
You are not specifying if getMessages is in a service, component... in any case, I suggest returning the Observable without subscribing to it in the getMessages function
// this function could be anywhere
getMessages(): Observable<string[]> {
return this.gatewayMessagesState$.pipe(
map((data) => data.gatewayMessages.get(this.gatewayId)),
map((yourMap) => yourMap?.list.map((theMessage) => theMessage.message))
);
}
Now, if you need to extract this value, either from a component, a service, etc... then, just call this function and then subscribe to get the result
Let's say getMessages is in a service file for example
Your component
constructor(private theService: YourService) {}
anotherFunction() {
this.theService.getMessages().subscribe((myMessages) => console.log(myMessages));
}
Or let the async pipe subscribe to this observable
Your component
messages$!: Observable<string[]>
constructor(private theService: YourService) {}
anotherFunction() {
this.messages$ = this.theService.getMessages()
}
Your component html
<ng-container *ngIf="messages$ | async as messages">
<div *ngFor="let message of messages">
<p>{{ message }}</p>
</div>
</ng-container>
I this you want to retrieve the data as an observable of messages as string, you can define the function return as this and using pipe and map operatoes from rxjs,this is code below is my proposition
getMessages(): observable<string[]>{
return this.gatewayMessagesState$.pipe(map((data) =>
data.filter((f) => f.gatewayMessages.id ===this.gatewayId)),
map(item => item.message));
}

Return a merged object composed by two different Observables with dependencies with RxJS

I'm going to explain the context I have before explain the problem design a service with Angular and RxJS.
I have one object with this model:
{
id: string;
title: string;
items: number[];
}
I obtain each object of this type (called "element") through GET /element/:id
Each element has an items: number[] that contains an array of numbers and each number has an URL so I do a new GET /element/:id/:itemNumber for each number and return the items detail. That item detail model is like:
{
idItem: string;
title: string;
}
So in my service I want to serve to the components a method that it will return an Observable, it will obtain an array of objects, where each object has the element model with one addition property that will be an array of its detailed items. So, I'm going to show what I have:
The service:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
map(element => this.getAllItemsDetail(element))
}
getAllItemsDetail(element: Element): any {
return of(...element.items).pipe(
map(val => this.getItemDetail(element.id, val)),
concatAll()
);
}
My problem is, I'm not understanding how I can, in RxJS, after the map(element => this.getAllItemsDetail(element)) in the getElement method, merge the items array returned by it and the previous element I have before the map that has the element object. I need to add "anything" after that map to compute an Object.Assign to merge the both objects.
EDIT
With the answer of #chiril.sarajiu, he gives me more than one clue, with switchMap I can map the observable (map is used for objects, "do a compute with the result object"). So my code, it's not the most pretty version I know..., I will try on it, but it works. So the final code looks like this:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element)),
map(result => { return Object.assign({}, result.element, {itemsDetail: result.items})})
);
}
getAllItemsDetail(element: Element): any {
const results$ = [];
for (let i = 0; i < element.items.length; i++) {
results$.push(this.getItemDetail(element.id, element.items[i]));
}
return zip(...results$).pipe(
map(items => { return Object.assign({}, { element, items })})
);
}
Be aware of the possibility about zip will not emit values if the results$ is empty, in that case you can add of(undefined) to the results$ array and after take into account that items will be [undefined]
To map another observable you should use switchMap function instead of map
So
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element))
}
UPDATE
Consider this medium article

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

Angular2 observable with JSON root element

This is my first time ever working with angular observables and I'm a bit confused on how this works. I was given a mostly functioning angular CLI app that I just need to wire up to my already existing API.
I have a service with this function
public getApps(): Observable<ApplicationInterface[]> {
return this.http.get(url);
}
Then in my component, I have
public data: ApplicationInterface[];
ngOnInit() {
this.route.params
.subscribe(params => {
this.fetchData();
});
}
fetchData() {
this.service.getApps()
.subscribe(data => {
this.data = data;
});
}
My api endpoint returns a JSON structure of {"applications": []}
I can't seem to figure out how to access the array in that JSON hash.
If I console.log(data) in the subscribe block, it is the API response with the applications key that I expect, but if I change the data assignment to this.data = data.applications, ng build fails with Property 'applications' does not exist on type 'ApplicationInterface[]'
You should design the interface to be aligned with the response. If the response is object, than you need to have it like this also in the interface.
Try something like this (using the new HttpClient):
interface ApplicationInterfaceResponse {
applications: ApplicationInterface[];
}
public getApps(): Observable<ApplicationInterface[]> {
return this.httpClient
.get<ApplicationInterfaceResponse>(url)
.map(response => {
console.log(response.applications);
return data.applications;
});
}
If your return is of type ApplicationInterface[], then it's an array of ApplicationInterfaces, thus does not have a property called applications on it. This has nothing to do with your observable; it's fine. Rather, you've mistyped your variable.
If you don't need any other properties of data, you can map the value:
public getApps(): Observable<ApplicationInterface[]> {
return this.http.get(url).map(data => data.applications);
}
However, I recommend against this in most situations. If your object changes in the future, then you have to change this function and all attached subscriptions. Instead, you should create an interface for your response (your response right now does not match the type you're giving it), and use values of it as necessary.
The simplest fix is to indicate the correct form of the data that is returned by your service method since it doesn't actually return an array:
public getApps(): Observable<{applications:ApplicationInterface[]}> {
return this.http.get(url);
}
Now in your subscribe, you can get at the array as you would expect
.subscribe(e => this.data = e.applications)

Ngrx observables not reacting to state store change when using Object.assign() in combination with other operations

I have some trouble understanding why the first sample seems to work ok but the second one has trouble firing the observers.
// Working reducer
return Object.assign({}, state, {
expanded: Object.assign({}, state.expanded, { clients: !state.expanded.clients })
});
// Faulty reducer - Devtools indicates a change but the observables
// seem to not respond and pass on the information down the line
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
// Selectors
import {createSelector} from 'reselect';
export const SIDEBAR = (state: AppState) => state.sidebar;
export const SIDEBAR_UI = createSelector<AppState, SidebarUIState, SidebarState>(
SIDEBAR,
(state: SidebarState) => state.ui
);
// Sidebar service
public getSidebarUIExpandedObservable(): Observable<SidebarUIExpandedState> {
debug('Get sidebar UI expanded observable');
return this._store.select(SIDEBAR_UI_EXPANDED);
}
Instead of doing :
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
do
return Object.assign(
{},
state,
{
expanded: Object.assign(
{},
state.expanded,
{
clients: !state.expanded.clients
}
)
}
);
Trick :
When you'll have Typescript >= 2.1.4 you'll be able to do it like that
return {
...state,
{
expanded: {
...state.expanded,
{
clients: !state.expanded.clients
}
}
}
};
Reducers require pure functions, that make use of immutable objects.
Hence you cannot mutate the newState object, but you would have to create a new object that has definite new state upon creation.
Your first example fits the immutability principle, because Object.assign copies the matching propertie(s) of the object in the third parameter to a copy of the object in the second parameter, before assigning this to target object (first parameter). Thus, the original state has remained unaffected, and a new state is obtained in the target object.
In other words, no original object state has been changed by object.assign in memory and the return value delivers a ready to go (no further mutations required ) new state object.
more info on Object.assign(): https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
In the second case there is a new object created at first (immutability still valid), subsequently, the state is mutated by assigning the boolean, hence the immutability principle is broken, and reducers usually do not accept this.
With the following in a reducer:
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
the content of state.expanded is being mutated.
That is newState.expanded and state.expanded refer to the same object and that object's client property is being mutated. Code looking at expanded will see that the object reference is unchanged and will assume that the content has not changed either, but you have mutated it by toggling the value of its clients property.

Categories

Resources