Avoid multiple request when using ngrx effect - javascript

I would like to make two treatments on a same api call data.
I have a first effect:
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.GetSchedulesByDate),
mergeMap(() =>
this.apiCallsService.getSchedulings().pipe(
map(trips => ({ type: ESchedulesActions.GetSchedulesByDateSuccess, payload: trips })),
catchError(() => EMPTY)
)
)
)
);
I call getSchedulings service method which make an api call then a treatment 1 on data is done
ApiCallsService :
getSchedulings() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2).pipe(
...
return groupByDate;
})
);
}
I would like to make a second treatment on the same data source. (raw data got from api ) but in parallel of the first because they are independent
So by logic I create a second effect
loadDirections$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.GetSchedulesByDate),
mergeMap(() =>
this.apiCallsService.getDirections().pipe(
map(trips => ({ type: ESchedulesActions.GetDirectionsByDateSuccess, payload: directions})),
catchError(() => EMPTY)
)
)
)
);
Then in apiCallService I should have a method
getDirections() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2).pipe(
...
return groupByDirections;
})
);
}
The problem here is that I will have two requests for the same data.
To summarize the actual workflow :
LoadSchedulings ( effect ) ==> loadSchedulings ( service ) ==> API Call ==> treatment 1
LoadDirections ( effect ) ==> loadDirections ( service ) ==>(Same) API Call ==> treatment 2
So I would like to only use the first api request's data for two treatments
Update: According to the response of Manuel Panizzo I should have something like this ?
getRawData() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2)
}
Effect.ts
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
pipe((data) =>
this.apiCallsService.getSchedulings(data).pipe(
map(trips => ({ type: ESchedulesActions.GetSchedulesByDateSuccess, payload: trips })),
catchError(() => EMPTY)
)
),
pipe((data) =>
this.apiCallsService.getDirections(data).pipe(
map(directions=> ({ type: ESchedulesActions.GetDirectionsByDateSuccess, payload: directions})),
catchError(() => EMPTY)
)
),
)
);

I think you could also dispatch a getRawDataSuccess action (that performs 1 api call)
getRawData$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
mergeMap(() =>
this.apiCallsService.getRawData().pipe(
map(data => ({ type: ESchedulesActions.GetRawDataSuccess, payload: data })),
catchError(err => ({ type: ESchedulesActions.GetRawDataError, payload: err }))
)
)
)
);
Then create one effect per treatment listening for getRawDataSuccess action:
getSchedulesByDate$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawDataSuccess),
map((action) => {
return {
type: ESchedulesActions.GetSchedulesByDateSuccess,
payload: action.payload.schedulesByDate,
}
})
)
);
getDirectionsByDate$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawDataSuccess),
map((action) => {
return {
type: ESchedulesActions.GetDirectionsByDateSuccess,
payload: action.payload.directionsByDate,
}
})
)
);
This would be cleaner IMO and will theoretically run in parallel too.

use only one effect to get the raw data from the API and put in your store then create two diferents selectors that aply your groupByDirections and groupByDate logic.
Or extract the groupByDirections and groupByDate logic to the effect. an make a pipe in your effect that aply both logics and dispatch two actions in the same effect
UPDATE:
if you want execute two actions try this:
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
mergeMap(action => this.apiCallsService.getRawData()),
map(rawApiData => {
const groupByDate = {}; // do your logic with rawApiData
const groupByDirections = {}; // do your logic with rawApiData
return { groupByDate, groupByDirections };
}),
mergeMap(groupedData => [
{
type: ESchedulesActions.GetDirectionsByDateSuccess,
payload: groupedData.groupByDirections,
},
{
type: ESchedulesActions.GetSchedulesByDateSuccess,
payload: groupedData.groupByDate,
},
]),
),
);

Related

Compare two arrays in react and filter one based on matches of name property

I'm trying to filter a players array by comparing the names property to a roster array and rendering the matches from the players array.
When a user selects a year the getCollegeRoster() function returns players who have a year property matching the selection. From there I'm trying to filter and display the .name matches from the players array, but it seems i'm unable to update playerStateValue. I'm using recoil to manage state. Any insight would be much appreciated.
const getCollegeRoster = async () => {
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
const onSelection = (newSelection: any) => {
setSelection(newSelection);
console.log('newselect', newSelection)
getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} ```
also tried adding setplayerstatevalue into the getcollegeroster function:
onst getCollegeRoster = async () => {
console.log("WE ARE GETTING Players!!!");
console.log('communitydata', communityData);
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
{playerStateValue.players.map((player: Player, index) => (
<PlayerItem
key={player.id}
player={player}
// postIdx={index}
onPlayerVote={onPlayerVote}
userVoteValue={
playerStateValue.playerVotes.find((item) => item.playerId === player.id)
?.voteValue
}
onSelectPlayer={onSelectPlayer}
/>
I believe I found the problem. getCollegeRoster() is an async function which means it is being executed asynchronous.
That function is responsible for updating roster and you are using roster inside your filtering while it is being asynchronously updated.
There is no guarantee that roster is updated by the time you filter your array.
You have two ways of solving this. Either you execute setPlayerStateValue() inside of getCollegeRoster() or you wait for getCollegeRoster() to be done by preappending it with await and your onSelection function with async:
const onSelection = async (newSelection: any) => {
setSelection(newSelection);
console.log("newselect", newSelection);
await getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
}));
...
Edit:
Another scenario I can think of is that playerStateValue might be the culprit. In React setState works asynchronous as well, if you access playerStateValue via curr/prev-callback, it is guaranteed that you are accessing the values of the current state object while updating the state. Try this use of setPlayerStateValue:
setPlayerStateValue((prev) => {
const filteredArray = prev.players.filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
})) as Player[];
return {
...prev,
players: filteredArray,
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray,
},
playerUpdateRequired: false,
};
});

Mapping obsevable to another list of promise observable

Here what i am trying to do:
I have pokemons from the api. pokeapi
The http response is list of object only includes name and url prop.
I need to map these url to observable values. I can basically use
Promise.all(...).then(data => observable$.next(data))
but this seems unelegant to me,
here is what i try
const __URL__ = 'https://pokeapi.co/api/v2/pokemon/';
const pokemon$ = from(
fetch(__URL__)
.then((res) => res.json())
.then((res) => res.results)
);
var pokemons$ = new BehaviorSubject([]);
pokemon$.subscribe((pokes) => {
Promise.all(pokes.map((p) => fetch(p.url).then((res) => res.json()))).then((pokks) =>
pokemons$.next(pokks)
);
});
I just wonder is there a method to use observable operators(map) to generate the result with a single observable elegantly.
You can achieve that using forkJoin function, like the following:
// import { BehaviorSubject, forkJoin, from } from 'rxjs';
// import { mergeMap } from 'rxjs/operators';
const __URL__ = 'https://pokeapi.co/api/v2/pokemon/';
const pokemon$ = from(
fetch(__URL__)
.then((res) => res.json())
.then((res) => res.results as Array<{ name: string; url: string }>)
);
const pokemons$ = new BehaviorSubject([]);
pokemon$
.pipe(
mergeMap((pokes) =>
forkJoin(
pokes.map((poke) => from(fetch(poke.url).then((res) => res.json())))
)
)
)
.subscribe((pokks) => pokemons$.next(pokks));

how to make nested bitbucket API http request in ngrx effects

BitBucket API 1.0 returns tags with no timestamp. Example response:
{
"size": 2,
"limit": 2,
"isLastPage": false,
"values": [
{
"id": "refs/tags/tag1",
"displayId": "tag1",
"type": "TAG",
"latestCommit": "ff7be6fad2e660e8139f410d2585f6b2c9867a61",
"latestChangeset": "ff7be6fad2e660e8139f410d2585f6b2c9867a61",
"hash": "f13db8e5c0b75b57b48777299d820525ad8127b9"
},
{
"id": "refs/tags/tag2",
"displayId": "tag2",
"type": "TAG",
"latestCommit": "4f5878c9554444755dbf6699eac33ff8752add5f",
"latestChangeset": "4f5878c9554444755dbf6699eac33ff8752add5f",
"hash": "23274bd5c9b87614f14a2245d5e70812c83104b7"
}
]
}
Therefore I am trying to request the latestCommit to get it's data. The response retrieves the committerTimestamp which I want to add to the tag object.
The ngrx effect and a following function are written as follows:
#Effect()
loadTags: Observable<LoadTagsRes> = this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
map((response: any) => response.values.map(tag => (
{
...tag,
timeStamp: this.getTagTimestamp(tag.latestCommit)
}
))
),
map((response: any) => new LoadTagsRes({tags: response}))
)
}),
);
getTagTimestamp(tag) {
return this.http.get<any>(`https://bitbucket/rest/api/1.0/projects/projects/repos/project/commits/${tag}`, httpOptions).pipe(
switchMap(res => {
return res;
})
);
}
the new timeStamp property in the array of objects displays the following in redux devtools:
Would like to get the correct response for the inner http request.
The function should trigger the secondary request and fetch the timestamp but it isn't being triggered yet. You need to use switchMap (or any other RxJS higher order mapping operator) to trigger it. And seeing that you have multiple requests, you could use RxJS forkJoin function to trigger them in parallel.
Try the following
#Effect()
loadTags: Observable<LoadTagsRes>= this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
switchMap((response: any) => {
forkJoin(response.values.map(tag =>
this.getTagTimestamp(tag.latestCommit).pipe(
map(timeStamp => ({...tag, timeStamp: timeStamp}))
)
))
}),
map((response: any) => new LoadTagsRes({
tags: response
}))
)
}),
);
loadTags: Observable<LoadTagsRes> = this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
mergeMap((response: any) => response.values.map(tag => {
// array of observables
const tag$=this.getTagTimestamp(tag.latestCommit);
// use forkJoin to return single observable that returns array of results
return forkJoin(tag$)
})
),
map((response: any) => new LoadTagsRes({tags: response}))
)
}),
);
getTagTimestamp(tag) {
return this.http.get<any>(`https://bitbucket/rest/api/1.0/projects/projects/repos/project/commits/${tag}`, httpOptions).pipe(
switchMap(res => {
return res;
})
);

action creator does not return value to stream in marble test

I've got following Epic which works well in application, but I can't get my marble test working. I am calling action creator in map and it does return correct object into stream, but in the test I am getting empty stream back.
export const updateRemoteFieldEpic = action$ =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
const { orderId, fields } = payload;
const requiredFieldIds = [4, 12]; // 4 = Name, 12 = Client-lookup
const requestData = {
id: orderId,
customFields: fields
.map(field => {
return (!field.value && !requiredFieldIds.includes(field.id)) ||
field.value
? field
: null;
})
.filter(Boolean)
};
if (requestData.customFields.length > 0) {
return from(axios.post(`/customfields/${orderId}`, requestData)).pipe(
map(() => queueAlert("Draft Saved")),
catchError(err => {
const errorMessage =
err.response &&
err.response.data &&
err.response.data.validationResult
? err.response.data.validationResult[0]
: undefined;
return of(queueAlert(errorMessage));
})
);
}
return of();
})
);
On successfull response from server I am calling queueAlert action creator.
export const queueAlert = (
message,
position = {
vertical: "bottom",
horizontal: "center"
}
) => ({
type: QUEUE_ALERT,
payload: {
key: uniqueId(),
open: true,
message,
position
}
});
and here is my test case
describe("updateRemoteFieldEpic", () => {
const sandbox = sinon.createSandbox();
let scheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
afterEach(() => {
sandbox.restore();
});
it("should return success message", () => {
scheduler.run(ts => {
const inputM = "--a--";
const outputM = "--b--";
const values = {
a: updateRemoteField({
orderId: 1,
fields: [{ value: "test string", id: 20 }],
update: true
}),
b: queueAlert("Draft Saved")
};
const source = ActionsObservable.from(ts.cold(inputM, values));
const actual = updateRemoteFieldEpic(source);
const axiosStub = sandbox
.stub(axios, "post")
.returns([]);
ts.expectObservable(actual).toBe(outputM, values);
ts.flush();
expect(axiosStub.called).toBe(true);
});
});
});
output stream in actual returns empty array
I tried to return from map observable of the action creator which crashed application because action expected object.
By stubbing axios.post(...) as [], you get from([]) in the epic - an empty observable that doesn't emit any values. That's why your mergeMap is never called. You can fix this by using a single-element array as stubbed value instead, e.g. [null] or [{}].
The below is an answer to a previous version of the question. I kept it for reference, and because I think the content is useful for those who attempt to mock promise-returning functions in epic tests.
I think your problem is the from(axios.post(...)) in your epic. Axios returns a promise, and the RxJS TestScheduler has no way of making that synchronous, so expectObservable will not work as intended.
The way I usually address this is to create a simple wrapper module that does Promise-to-Observable conversion. In your case, it could look like this:
// api.js
import axios from 'axios';
import { map } from 'rxjs/operators';
export function post(path, data) {
return from(axios.post(path, options));
}
Once you have this wrapper, you can mock the function to return a constant Observable, taking promises completely out of the picture. If you do this with Jest, you can mock the module directly:
import * as api from '../api.js';
jest.mock('../api.js');
// In the test:
api.post.mockReturnValue(of(/* the response */));
Otherwise, you can also use redux-observable's dependency injection mechanism to inject the API module. Your epic would then receive it as third argument:
export const updateRemoteFieldEpic = (action$, state, { api }) =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
// ...
return api.post(...).pipe(...);
})
);
In your test, you would then just passed a mocked api object.

How to request data sequentially in Cycle.js?

I’m new to reactive programming and toying around with cycle.js, trying to implement who to follow box from this tutorial. But I understood that for proper implementation (and learning purposes) I don’t have one piece of data: full user name. I can get it by sequentially getting users and then full user data from server. In imperative style I would do something like this:
fetch(`https://api.github.com/users`)
.then(data => data.json())
.then(users => fetch(users[0].url))
.then(data => data.json())
.then(/* ... work with data ... */)
But how do I do it in cycle?
I’m using fetch driver and trying something like this:
function main({ DOM, HTTP }) {
const users = `https://api.github.com/users`;
const refresh$ = DOM.select(`.refresh`).events(`click`)
const response$ = getJSON({ key: `users` }, HTTP)
const userUrl$ = response$
.map(users => ({
url: R.prop(`url`, R.head(users)),
key: `user`,
}))
.startWith(null)
const request$ = refresh$
.startWith(`initial`)
.map(_ => ({
url: `${users}?since=${random(500)}`,
key: `users`,
}))
.merge(userUrl$)
const dom$ = ...
return {
DOM: dom$,
HTTP: request$,
};
}
where getJSON is
function getJSON(by, requests$) {
const type = capitalize(firstKey(by));
return requests$
[`by${type}`](firstVal(by))
.mergeAll()
.flatMap(res => res.json());
And I’m always getting some cryptic (for me) error like: TypeError: Already read. What does it mean and how do I handle it properly?
You were quite close. You just need to remove startWith(null) as a request, and grabbing the second response (you were missing the getJSON for that one).
function main({ DOM, HTTP }) {
const usersAPIPath = `https://api.github.com/users`;
const refresh$ = DOM.select(`.refresh`).events(`click`);
const userResponse$ = getJSON({ key: `user` }, HTTP);
const listResponse$ = getJSON({ key: `users` }, HTTP);
const userRequest$ = listResponse$
.map(users => ({
url: R.prop(`url`, R.head(users)),
key: `user`,
}));
const listRequest$ = refresh$
.startWith(`initial`)
.map(_ => ({
url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
key: `users`,
}));
const dom$ = userResponse$.map(res => h('div', JSON.stringify(res)));
return {
DOM: dom$,
HTTP: listRequest$.merge(userRequest$),
};
}
Because inquiring minds want to know...here's a complete working example:
import Cycle from '#cycle/rx-run';
import {div, button, makeDOMDriver} from '#cycle/dom';
import {makeFetchDriver} from '#cycle/fetch';
import R from 'ramda'
function main({DOM, HTTP}) {
const usersAPIPath = 'https://api.github.com/users';
const refresh$ = DOM.select('button').events('click');
const userResponse$ = getJSON({ key: 'user' }, HTTP);
const listResponse$ = getJSON({ key: 'users' }, HTTP);
const userRequest$ = listResponse$
.map(users => ({
url: R.prop('url', R.head(users)),
key: 'user',
}));
const listRequest$ = refresh$
.startWith('initial')
.map(_ => ({
url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
key: 'users',
}));
const dom$ = userResponse$.map(res => div([
button('Refresh'),
div(JSON.stringify(res))
]));
return {
DOM: dom$,
HTTP: listRequest$.merge(userRequest$)
};
function getJSON(by, requests$) {
return requests$.byKey(by.key)
.mergeAll()
.flatMap(res => res.json());
}
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
HTTP: makeFetchDriver()
});
It took me a while to figure out HTTP was the #cycle/fetch driver, and NOT the #cycle/http driver. Next, a bit of searching turned the ramda npm library providing prop and head methods.

Categories

Resources