Where should the "id" of the new data object be created/assigned? - ngrx

I'm building a web app using Angular 7, and want to use #ngrx/store and #ngrx/effects.
Disclaimer: I'm new to these technologies.
In a typical client-server application, I would think it makes sense to allow the database to assign the id value, which would then be passed back to the client in the HTTP response.
But given that API calls are supposed to happen in side effects when using NGRX, what's the proper way to update the new object in the state with the id returned in the http response?
My understanding is that side effects are not supposed to manipulate state. That's the whole reason they're called side effects.
What's the proper way to solve this problem?

Some possibilities:
You can let the client generate there own ID's.
You can only add entities to the store when the server roundtrip is finished.
This can be done by returning the whole object back from the server, or to append the id to the entity in an effect:
#Effect()
save = this.actions.pipe(
ofType(Foo),
exhaustMap(({ payload }) =>
this.service.post(payload).pipe(
map(
result =>
new FooSuccess({
...payload,
id: result,
})
),
catchError(() => of(new FooFailed()))
)
)
);
State manipulations are indeed done via reducers, if you can somehow link the new entity in the store with the returned payload you could update the id via the reducer
#Effect()
save = this.actions.pipe(
ofType(Foo),
exhaustMap(({ payload }) =>
this.service.post(payload).pipe(
map(
result =>
new FooSuccess({
reference: payload.reference,
id: result,
})
),
catchError(() => of(new FooFailed()))
)
)
);
// and in the reducer
return {
...state,
// mutate found entity, else just return the entity in the store
entities: this.state.entities.map(p => p.reference === action.reference ? mutateEntityHere : p)
}
My preferences would be either the first or second option.

Related

How to properly handle simultaneous persistence actions in Redux?

React application using Redux. A have a combined reducer, consisting of appStateReducer and contestReducer. Each of these two takes care of some part of the application data.
When action is performed, I want not only the respective state to be changed, but I also want to persistently save the new state, so that if the user reloads application page in the browser, the state would be preserved.
My idea is to add third reducer to take care only of save and load actions (each of the two sub-states separately).
Save and load will use IndexedDB, through localbase package. All of the db actions (add, get, update, delete) appear to be synchronous, i.e. there seems to be no real need to implement asynchronous actions. UPDATE: this is wrong, it is asynchronous, just some basic examples ignore it.
I am not sure how to handle the problem properly.
I will need a database connection object, a singleton, initialized once after page is loaded, which should be shared by all save/load actions regardless of which part of the state is to be stored or loaded. That would lead to a separate reducer working only with the db object. If I do this, the db reducer would have to have access to all the other sub-state, which is normally not the case in Redux.
Or, I could implement save and load action in each reducers separately, not a big deal, actually. But how to make the global db object accessible by the reducers?
It is as React application written in typescript and all components are implemented as classes.
You already have access to all data if you are using middleware, Example:
export const requestPost = (id) => (dispatch,getState) => {
// You can make an bank for post and check If data exist or not
const postState = getState().bank.posts.data;
const found = postState?.find((post) => post.id === id);
if (found) {
dispatch({ type: SUCCESS.POST, data: found });
} else {
dispatch({ type: REQUEST.POST });
API.get(`/post/v2?id=${id}`)
.then((res) => dispatch({ type: SUCCESS.POST, data: res.data[0] }))
.catch((err) => errorHandler(err, FAILURE.POST));
}
};
Just make and reducer for saving data on DB or somewhere and read them at the start.

Vuex - data from different modules are returned in random order when using then()

In one of my vuex modules I'm loading data 3 times step by step with 3 different API requests using then():
actions: {
loadRoamingHistory: function loadRoamingHistory(context, roamingFilter): Promise<Array<RoamingHistoryEvent>> {
return new Promise((resolve) => {
store.dispatch('network/loadNetworks').then(() => {
store.dispatch('country/loadCountries').then(() => {
providerApi.loadRoamingHistory(roamingFilter).then(data => {
// read already loaded networks and countries from store
let networks = context.rootState.network.networks;
let countries = context.rootState.country.countries;
// .. some data processing using informations from
// networks and countries request, that are not allways available at this point..
console.log('data processing');
commitSetRoamingHistoryEvents(context, data.roamingHistoryEvent);
resolve();
}).catch(err => console.log(err));
});
});
});
}
}
I also added console.log() commands to the network and country vuex setters in order to see what is executed first:
mutations: {
setNetworks: function setNetworks(state: NetworkState, networks: Array<Network>) {
console.log('networks loaded');
state.networks = networks;
},
I would expect the 3 requests to be executed one by one, but the log messages shows that sometimes it is executed in different order, for example log messages goes like this:
networks loaded
countries loaded
networks loaded
data processing
countries loaded
Notice that data processing should be last log, otherwise I cannot process the data correctly. Why it is executed in random order and what could be done in order to fix it?
first of all, I need to correct myself, dispatch is an action, it is asynchronous, so you're correct to use promises with them. (I'm used to mapping actions, so I don't see them much)
anyway, the point of promises was to alleviate the "callback hell". so if your structure is nested like this:
action
action
action
action
you're defeating the point of using a promise in the first place.
Instead the point is to chain them in a readable fashion like so
action
action
action
action
actions: {
loadRoamingHistory: function loadRoamingHistory(context, roamingFilter): Promise<Array<RoamingHistoryEvent>> {
return store.dispatch('network/loadNetworks')
.then(() => {
return store.dispatch('country/loadCountries')
})
.then(() => {
return providerApi.loadRoamingHistory(roamingFilter)
})
.then(data => {
// read already loaded networks and countries from store
let networks = context.rootState.network.networks;
let countries = context.rootState.country.countries;
// .. some data processing using informations from
// networks and countries request, that are not allways available at this point..
console.log('data processing');
return commitSetRoamingHistoryEvents(context, data.roamingHistoryEvent);
})
.catch(err => console.log(err));
}
}
note that...
- the initial promise is not defined. Because the dispatch is asynchronous, it already creates a promise, we just add additional calls to it.
- when returning a promise inside a promise, the next then() will handle it, whether it is withing this function or outside of it
- your catch at the end will log an error anywhere along the chain of promises

Issue with clearing state from an ngrx/redux store when the user logs out

My application uses ngrx/rxjs. I rely on an ngrx effect in order to signout and clear state from the store.
Unfortunately, because one of my components subscribes to the store through a selector (see below: getLatestMessagesByCounterParty) and because the state is cleared before this component is destroyed, I get the following error:
ERROR TypeError: Cannot read property 'id' of null
at getCurrentUserAccountId
... indicating that the currentUserAccount is null, which is quite logical since I have just cleared the state from the store.
Here is the signout$ effect:
#Effect()
signout$: Observable<Action> = this.actions$
.ofType(authenticated.ActionTypes.SIGNOUT)
.switchMap(() =>
this.sessionSignoutService.signout()
.do(() => {
localStorage.removeItem('authenticated');
localStorage.removeItem('sessionToken');
})
.concatMap(() => [
new ClearMessagesAction(null),
new ClearUserAccountAction(null),//Error thrown here...
go(['/signin'])//Never reached...
]));
Here is the component that subscribes to the logged-in state:
ngOnInit() {
this.store.select(fromRoot.getLatestMessagesByCounterParty)
.subscribe(latestMessages => this.latestMessages = this.messageService.sortMessagesByDate(latestMessages, this.numberOfConversations));
}
And the relevant selectors:
...
const getCurrentUserAccountId = (state: State) => state.userAccount.currentUserAccount.id;
const getMessagesState = (state: State) => state.message.messages;
...
export const getLatestMessagesByCounterParty = createSelector(getCurrentUserAccountId, getMessagesState, fromMessage.latestMessagesByCounterParty);
I am looking for best practices on where, when and how to clear state from the store. Ideally I would like to do that at the last possible time, when the subscribing components have been destroyed.
Can someone please advise?
edit: Let me further refine my comment. My code above should have read as follows.
.concatMap(() => [
new ClearMessagesAction(null),
new ClearUserAccountAction(null),//Error thrown right after this action because selector cannot find id variable on state
go(['/signin'])//Never reached...
]));
As #cgatian said, you might use a filter. But here's what would happen behind the scene with that code :
.concatMap(() => [
new ClearMessagesAction(null),
new ClearUserAccountAction(null),//Error thrown here...
go(['/signin'])//Never reached...
]));
You'd first dispatch an action ClearMessagesAction(null).
Then that action will be handled by your reducer.
___A new state will be produced
___Your selectors will be triggered right after
___An error will occur because you end up with an inconsistent store (as you expect that the other action ClearUserAccountAction(null) was dispatched as the same time and before the selectors kick in)
What you should do to avoid state inconsistency, is either :
- Create one action that you handle in both reducers. This way, your reducers will both be modified and only then, selectors will kick in
- Use a library that allows you to dispatch multiples actions as one (like redux-batched-actions). This way you could write something like that :
batchActions([
new ClearMessagesAction(null), --> selectors not triggered yet
new ClearUserAccountAction(null) --> selectors not triggered yet
]); --> selectors triggered now

How to complete 2 http requests and then trigger another Action to merge the results

I have equivalent data types stored on 2 different databases.
In a SQL database, I am storing a massive list of all foods.
On a Mongo database I am storing an individual's food data which will
override the default food data provided.
Both types of data will be stored in my ngrx store Application State as well as a Merged State.
export interface ApplicationState {
allSQLFoodData: FoodData;
allUserFoodData: FoodData;
mergedFoodData: FoodData;
}
What I need to do, is to get the allSQLFoodData and the allUserFoodData from the backends, store those value to the state and also merge both sets of data in the merged state.
I'm doing this by dispatch()ing an action INITIALIZE_DATA_ACTION which I then listen for in an effect service.
Here is where I stand in my effect service:
initialize-data-effect-service.ts
#Injectable()
export class InitializeDataEffectService {
#Effect() initialize$: Observable<Action> = this.actions$
.ofType(INITIALIZE_DATA_ACTION)
.switchMap( (action) => {
return Observable.combineLatest(
this.foodService.loadSQLFoods( action.payload ),
this.userDataService.loadUserData( action.payload )
)
})
.switchMap(([allSQLFoodData, allUserData]) => {
return Observable.of(
new SQLFoodsLoadedAction( allSQLFoodData ),
new UserDataLoadedAction( allUserData ),
new BuildMergedFoodDataAction({sql: allSQLFoodData, user: allUserData})
)
})
constructor( private actions$: Actions, private foodService: FoodService, private userDataService: UserDataService ) {
}
}
As you can see, I am listening for the action of type INITIALIZE_DATA_ACTION and creating a combined observable using combineLatest() from the http responses. I then invoking the Action classes to set the Application state for each:
.switchMap(([allSQLFoodData, allUserData]) => {
return Observable.of(
new SQLFoodsLoadedAction( allSQLFoodData ),
new UserDataLoadedAction( allUserData ),
new BuildMergedFoodDataAction({sql: allSQLFoodData, user: allUserData})
)
})
But this is giving the following error:
The type argument for type parameter 'T' cannot be inferred from the
usage. Consider specifying the type arguments explicitly. Type
argument candidate 'SQLFoodsLoadedAction' is not a valid type argument
because it is not a supertype of candidate 'UserDataLoadedAction'.
Types of property 'payload' are incompatible.
Type 'AllUserData' is not assignable to type 'AllSQLFoodData'.
Property 'foods' is missing in type 'AllUserData'.
I'm not sure how to
specifying the type arguments explicitly
and even if I did, I'm not sure how the Effect() decorator will now how to assign the results to the appropriate state values. how can I achieve this?
To return an observable from switchMap that emits multiple actions from the effect, use Observable.from, which can take an array:
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/concatMap';
...
.concatMap(([allSQLFoodData, allUserData]) => Observable.from([
new SQLFoodsLoadedAction( allSQLFoodData ),
new UserDataLoadedAction( allUserData ),
new BuildMergedFoodDataAction({sql: allSQLFoodData, user: allUserData})
]))
Note that the switchMap is unecessary, as the returned observable will emit immediately. You can just use concatMap, instead.
Also, the effect doesn't assign anything to the state. Its only role is to emit actions. Those actions will be handled by reducers that will update the appropriate parts of the application's state.

Rxjs: add data to elements of array returned from http response

Following this question: Add data to http response using rxjs
I've tried to adapt this code to my use case where the result of the first http call yields an array instead of a value... but I can't get my head around it.
How do I write in rxjs (Typescript) the following pseudo code?
call my server
obtain an array of objects with the following properties: (external id, name)
for each object, call another server passing the external id
for each response from the external server, obtain another object and merge some of its properties into the object from my server with the same id
finally, subscribe and obtain an array of augmented objects with the following structure: (external id, name, augmented prop1, augmented prop2, ...)
So far the only thing I was able to do is:
this._appService
.getUserGames()
.subscribe(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
_.forEach(this._userGames, game => {
this._externalService
.getExternalGameById(game.externalGameId)
.subscribe(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
});
});
});
Thanks in advance
I found a way to make it work. I'll comment the code to better explain what it does, especially to myself :D
this._appService
.getUserGames() // Here we have an observable that emits only 1 value: an any[]
.mergeMap(games => _.map(games, game => this._externalService.augmentGame(game))) // Here we map the any[] to an Observable<any>[]. The external service takes the object and enriches it with more properties
.concatAll() // This will take n observables (the Observable<any>[]) and return an Observable<any> that emits n values
.toArray() // But I want a single emission of an any[], so I turn that n emissions to a single emission of an array
.subscribe(games => { ... }); // TA-DAAAAA!
Don't use subscribe. Use map instead.
Can't test it, but should look more like this:
this._appService
.getUserGames()
.map(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
return this._userGames.map(game => { /* this should return an array of observables.. */
return this._externalService
.getExternalGameById(game.externalGameId)
.map(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
return game;
});
});
})
.mergeAll()
.subscribe(xx => ...); // here you can subscribe..

Resources