I'm somehow struggling to come up with marble diagram when using forkJoin. My output somehow always says that subscription is empty.
Pseudo code:
// epic
const epic = (action$) => action$.pipe(
ofType('SAVE'),
mergeMap(() => {
const requests = [
getJSON('/a'),
getJSON('/b'),
getJSON('/c'),
];
return forkJoin(
...requests
).pipe(
mergeMap(() => [
{ type: 'SAVE_DONE'},
{ type: 'LOAD'},
])
);
})
);
// mock
const dependencies = {
getJSON: url => cold('g', {
g: { url }
})
};
// input
hot('i', {
i: { type: 'SAVE' }
}
// output??
forkJoin goes in parallel, but in marble diagram it's sequential ? ggg ? Cause afaik if we look a the whole flow it's igggxy where x and y are the SAVE_DONE and LOAD actions. Or am I completely misunderstanding this?
if the test of epic you should expect 2 emits of SAVE_DONE and LOAD one right after another, for that you need parentheses.
Then the diagram should look like:
cold('(ab)', {
a: { type: 'SAVE_DONE'},
b: { type: 'LOAD'},
})
};
for the dependency of getJSON is up to you, there you can actually to simulate errors, or delays in the load, the longest is the winner.
const dependencies = {
getJSON: url =>
url === '/a'
? cold('--a|', {
a: { url }
}) :
url === '/b'
? cold('---a|', {
a: { url }
}) :
url === '/c'
? cold('----a|', {
a: { url }
}) :
cold('|', {}),
};
and because we have delay of 40ms the main diagram should be like
cold('----(ab)', {
a: { type: 'SAVE_DONE'},
b: { type: 'LOAD'},
})
};
Related
I have this app that I'm working on that is using RTK and in the documentation for selecting values from results, in queries using RTK Query, they have an example with a createSelector and React.useMemo. Here's that code and the page
import { createSelector } from '#reduxjs/toolkit'
import { selectUserById } from '../users/usersSlice'
import { useGetPostsQuery } from '../api/apiSlice'
export const UserPage = ({ match }) => {
const { userId } = match.params
const user = useSelector(state => selectUserById(state, userId))
const selectPostsForUser = useMemo(() => {
const emptyArray = []
// Return a unique selector instance for this page so that
// the filtered results are correctly memoized
return createSelector(
res => res.data,
(res, userId) => userId,
(data, userId) => data?.filter(post => post.user === userId) ?? emptyArray
)
}, [])
// Use the same posts query, but extract only part of its data
const { postsForUser } = useGetPostsQuery(undefined, {
selectFromResult: result => ({
// We can optionally include the other metadata fields from the result here
...result,
// Include a field called `postsForUser` in the hook result object,
// which will be a filtered list of posts
postsForUser: selectPostsForUser(result, userId)
})
})
// omit rendering logic
}
So I did the same in my app, but I thought that if it's using the createSelector then it can be in a separate slice file. So I have this code in a slice file:
export const selectFoo = createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
(result: { data?: TypeOne[] }, status: TypeTwo) => status,
],
(data: TypeOne[] | undefined, status) => {
return data?.filter((d) => d.status === status) ?? [];
}
);
Then I created a hook that uses this selector so that I can just pass in a status value and get the filtered results. This is in another file as well.
function useGetFooByStatus(status: WebBookmkarkStatus) {
const selectFooMemoized = useMemo(() => {
return selectFoo;
}, []);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result, status),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
Then lastly I'm using this hook in several places in the app.
The problem then is when I'm causing a re-render in another part of the app causes the query hook to run again (I think), but the selector function runs again, not returning the memoized value, even though nothing has changed. I haven't really figured it out what causes the re-render in another part of the app, but when I do the following step, it stops re-rendering.
If I replace the selector function in the useGetFooByStatus with the same one in the slice file. With this, the value is memoized correctly.
(Just to remove any doubt, the hook would look like this)
function useGetFooByStatus(status: TypeTwo) {
const selectFooMemoized = useMemo(() => {
return createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
(result: { data?: TypeOne[] }, status: TypeTwo) =>
status,
],
(data: TypeOne[] | undefined, status) =>
data?.filter((d) => d.status === status) ?? []
);
}, []);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result, status),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
Sorry for the long question, just want to try and explain everything :)
Solution 1 has one selector used for your whole app. That selector has a cache size of 1, so if you call it always with the same argument it will not recalculate, but if you call it with 1 and then with 2 and then with 1 and then with 2 it will always recalculate in-between and always return a different (new object) as a result.
Solution 2 creates one such selector per component instance.
Now imagine two different components calling these selectors - with two different queries with two different results.
Solution 1 will flip-flop and always create a new result - solution 2 will stay stable "per-component" and not cause rerenders.
Does the following work:
const EMPTY = [];
const createSelectFoo = (status: TypeTwo) => createSelector(
[
(result: { data?: TypeOne[] }) => result.data,
],
(data: TypeOne[] | undefined) => {
return data?.filter((d) => d.status === status) ? EMPTY;
}
);
function useGetFooByStatus(status: TypeTwo) {
//only create selector if status changes, this will
// memoize the result when multiple components
// call this hook with different status in one render
// cycle
const selectFooMemoized = useMemo(() => {
return createSelectFoo(status);
}, [status]);
const { foos, isFetching, isSuccess, isError } =
useGetFoosQuery(
"key",
{
selectFromResult: (result) => ({
isError: result.isError,
isFetching: result.isFetching,
isSuccess: result.isSuccess,
isLoading: result.isLoading,
error: result.error,
foos: selectFooMemoized(result),
}),
}
);
return { foos, isFetching, isSuccess, isError };
}
You may want to make your component a pure component with React.memo, some more information with examples of selectors can be found here
I have an NGRX effect that - depending on the state - emits an action with a delay or it emits nothing.
I want to write a test, covering both situations.
This is the effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
filter(state => state.foo === false),
delay(4000),
map(state => myOtherAction())
)
);
The test for the situation where it should emit the otherAction with the delay works fine:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});
But I have no clue how to test the other state, where it should NOT emit another action (the stream has zero length):
it('should return an empty stream', () => {
store.setState({
myFeature: {
foo: true
}
});
// ???
});
Please help :)
This will be tough to do because the filter will prevent the effect to ever return an observable.
Option 1:
// wherever you're dispatching MyAction, only dispatch it if the foo property is true on the created action
Option 2:
// Change the structure of the effect to return empty
import { EMPTY } from 'rxjs';
....
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
delay(4000),
map(state => state.foo ? myOtherAction() : EMPTY)
)
);
The test:
import { EMPTY } from 'rxjs';
....
describe('emit my action', () => {
const action = MyAction();
action.foo = false; // set foo property to false
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: EMPTY // assert now it is empty
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
})
Due to the hint from AliF50, I replaced the filter in the chain (which stops the Observable from emitting), by a "Noop Action" (= a normal action without any listeners on it). So instead of checking the foo property in the filter, I return the noopAction in the map, when foo is true, and the otherAction when it's false.
The effect:
myEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(MyAction),
//filter(state => state.foo === false),
delay(4000),
map(state => state.foo !== false ? noopAction() : myOtherAction())
)
);
The test:
describe('emit my action', () => {
const action = MyAction();
it('should return a stream with myOtherAction', () => {
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: MyOtherAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
it('should return a stream with noop action as foo is true', () => {
store.setState({
myFeature: {
foo: true
}
});
const scheduler = getTestScheduler();
scheduler.run(helpers => {
// build the observable with the action
actions = hot('-a', { a: action });
// define what is the expected outcome of the effect
const expected = {
b: NoopAction()
};
helpers.expectObservable(effects.myEffect$).toBe('- 4000ms b', expected);
});
});
});
We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}
I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?
Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.
you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });
You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });
The following code works. It does an ajax request and then call 2 actions, on at a time:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL)).map(response => {
return finishLoadingThings(response);
}).map(() => {
return sendNotification('success');
});
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}}
But this code does not:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL).switchMap(response => {
return concat(of(finishLoadingThings(response)),
of(sendNotification('success')));
});
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}
I've replace the map by a switchMap to merge 2 actions together (as seen in many other post). It works in the catch if an exception is thrown. I'm wondering whats wrong with the code. I'm guessing it's because I can't seem to really grasp when to use: map, swicthMap and mergeMap.
sendNotification and finishLoadingthings returns action object:
export function finishLoadingThings(data: any) {
return {
type: FINISH_LOADING_THINGS,
data,
};
}
Thanks!
The code provided as-is appears to work as intended: https://jsbin.com/becapin/edit?js,console I do not receive a "invalid object where stream expected" error when the ajax succeeds or fails.
Are you sure the error is coming from this code?
On a separate note, you might be happy to hear that Observable.of supports an arbitrary number of arguments, each one will be emitted after the other. So instead of this:
.switchMap(response => {
return concat(of(finishLoadingThings(response)),
of(sendNotification('success')));
});
You can just do this:
.switchMap(response => {
return of(
finishLoadingThings(response),
sendNotification('success')
);
});
This would not have caused a bug though, it's just cleaner.
I manage to fix my problem, by doing the switchMap at the same level than the mergeMap. Like this:
export const loadThingsEpic = action$ => {
return action$.ofType(LOAD_THINGS)
.mergeMap(({things}) => {
const requestURL = `${AppConfig.serverUrl()}/data/things`;
return ajax.getJSON(requestURL).switchMap(response => {
return of(response);
});
})
.switchMap((res) => {
return concat(of(finishLoadingThings(res.value)),
of(sendNotification('success')));
})
.catch(e => {
return concat(of(finishLoadingThings({ things: {} })),
of(sendNotification('error')));
});
}
Don't quite get it yet.