I'm using redux-persist and redux-observable in my application and noticed that my API calls wrapped in observable are happening before store state is restored. And restoration overrides fetched value with stale data.
How could rootEpic be paused, while buffering, until persist/REHYDRATE action arrives?
If you wanna wait until an "event" occurred you can use .skipUntil. For example:
const action$ = new Rx.Subject();
// In your epic
action$
.skipUntil(action$.filter(({ type }) => type === 'hydrated'))
.subscribe(x => console.log(x));
// "Rehydrate"
action$.next({ type: 'hello!' });
action$.next({ type: 'hello?' });
action$.next({ type: 'any body there?' });
action$.next({ type: 'hydrated' });
action$.next({ type: 'do' });
action$.next({ type: 'you' });
action$.next({ type: 'see' });
action$.next({ type: 'me' });
action$.next({ type: 'now' });
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I guess, in order to not add this to all of your epics, you could .mergeMap() your root epic or something :)
After discussion in the comments below:
This issue might also occur with other middleware. Thus, it might be a better solution to not fire any actions until the re-hydration is done.
I'm not familiar with redux-persist, but it may or may not be important to note that the order in which you pass in middleware to redux matters. Some middleware needs to be before or after others. If this sounds relevant to your issue, you might try swapping them.
Related
All of my API calls are handled by redux-sagas. I'm creating a heartbeat modal in my app to detect inactivity. Each time a saga goes off I want to clear my setTimeout so I know that the user is active.
My middleware is a basic one at the moment:
const heartbeatMonitor => store => next => action {
if (action['##redux-saga/SAGA_ACTION']) {
clearTimeout(window.myTimeout);
}
window.myTimeout = window.setTimeout(function() {
// send off an action to tell user they are inactive
}, 100000);
}
It seems like looking for this symbol, ##redux-saga/SAGA_ACTION, is the only way to tell if the action is a saga. I see that redux-sagas has a createSagaMiddleware(options) and I tried using effectMiddlewares but it doesn't seem like you have access to the dispatch method in there so I can't send off a new actions.
but it doesn't seem like you have access to the dispatch method in there so I can't send off a new actions.
Not sure whether this is the kind of solution you wanted, but you do have access to the dispatch method where your comment // send off an action to tell user they are inactive is located in your code snippet, as it is exposed by the store object. (this is documented in the Store Methods Section of the store in the redux docs)
Therefore something like the following should satisfy your case:
const heartbeatMonitor => store => next => action {
if (action['##redux-saga/SAGA_ACTION']) {
clearTimeout(window.myTimeout);
}
const { dispatch } = store;
window.myTimeout = window.setTimeout(() => {
dispatch({ type: "USER_INACTIVE" });
}, 100000);
}
Note: I would probably implement this differently (using redux-sagas effects) Maybe this is an option for you too:
Example Saga
import { put, delay } from "redux-saga/effects";
function* inactiveSaga() {
yield delay(100000);
yield put({ type: "USER_INACTIVE" })
}
Example Integration of saga above:
(add the following in your root saga)
//import { takeLatest } from "redux-saga/effects";
takeLatest(() => true, inactiveSaga)
Explanation: Every action will trigger the inactiveSaga (cause () => true). The inactiveSaga will wait 100000ms before dispatching the "inactive action". If there is a new action within this waiting time the previous execution of the inactiveSaga will be canceled (cause takeLatest, see redux-saga effect docs for takeLatest) and started from the beginning again. (Therefore no "inactive action" will be sent and the inactiveSaga will start to wait for these 100000ms again, before being cancelled or completing the delay and dispatching the "inactive action")
I am new in Redux and Jest and I am struggling on a problem. I want to write the test for this file:
eventListeners.js
import store from '#/store';
chrome.runtime.onMessage.addListener((request) => {
if (request.type === 'OAUTH_SESSION_RESTORED') {
store.dispatch(completeLogin());
}
});
I have this file:
eventListeners.test.js
it('dispatches completeLogin when OAUTH_SESSION_RESTORED received', () => {
// I have made a mock of `chrome.runtime.sendMessage` so the listener defined in eventListeners.js is called when doing that
chrome.runtime.sendMessage({ type: 'OAUTH_SESSION_RESTORED' });
// I want to test that store.dispatch got called
});
However I don't succeed to test that the dispatch method of the store is called.
What I have tried so far:
1) trying to mock directly the method dispatch of the store (eg. doing jest.spyOn(store, 'dispatch') , jest.mock('#/store')).
However nothing seems to work. I think it is because the store used in eventListeners.js is not the one in the specs. So, mocking it does not do anything
2) Using the redux-mock-store library, as described in https://redux.js.org/recipes/writing-tests .
Doing
const store = mockStore({})
chrome.runtime.sendMessage({ type: 'OAUTH_SESSION_RESTORED' });
expect(store.getActions()).toEqual([{ type: 'LOGIN_COMPLETE' }])
However, same issue (I guess): the store used in the spec is not the same as in the eventListeners.js . store.getActions() returns [].
Is there a good way to test that store.dispatch get called?
===================================
For now, what I do is that I subscribe to the store and I try to see if the store has change. As described in https://github.com/reduxjs/redux/issues/546
it('dispatches completeLogin when OAUTH_SESSION_RESTORED received', () => {
const storeChangedCallback = jest.fn()
store.subscribe(storeChangedCallback)
chrome.runtime.sendMessage({ type: 'OAUTH_SESSION_RESTORED' });
expect(storeChangedCallback).toHaveBeenCalled();
})
Is there a better way? Did I missed something?
Thank you for your answers.
I'm using ngrx in an Angular project. In this example I have an array of requests. I want to dispatch an action after each request but also after all are done.
So far I have something looking like this:
Observable.forkJoin(requests).pipe(
map(() => new actions.requestsSuccessful()),
catchError(() => of(new actions.requestsFailed()))
);
where requests is an array of Observables.
The code above works fine, when all requests are done, my requestsSuccessful() action is correctly dispatched.
However, I'm implementing a progressbar, which I want to update after each request has been made, but I also want to keep the dispatch of the action where all requests have been made.
I can't figure out how to dispatch an action after each request while keeping the action when everything is done.
Any ideas?
forkJoin emits only when all Observables complete so it's not useful here. Instead, you can use concatAll and concat.
This is model example simulating what you want if I understand you correctly.
const makeRequest = (v) => of(v)
.pipe(
delay(1000), // Simulate delay
map(response => ({ action: 'WHATEVER', response })), // Map response into action
);
const requests = [makeRequest(1), makeRequest(2), makeRequest(3)];
from(requests)
.pipe(
concatAll(), // Execute Observables in order one at the time
concat(of({ action: 'ALL_DONE' })), // Append this when all source Observables complete
)
.subscribe(console.log);
See live demo (open console): https://stackblitz.com/edit/rxjs6-demo-zyhuag?file=index.ts
This demo will print the following output:
{action: "WHATEVER", response: 1}
{action: "WHATEVER", response: 2}
{action: "WHATEVER", response: 3}
{action: "ALL_DONE"}
Btw, in future RxJS versions there will be endWith operator that you can use instead of concat that makes it more readable. https://github.com/ReactiveX/rxjs/pull/3679
Haven't tested it. Maybe this works.
let progress=0
Observable.forkJoin(requests.map(e=>e.do(()=>progress++)).pipe(
map(() => new actions.requestsSuccessful()),
catchError(() => of(new actions.requestsFailed()))
);
I am having an issue with the ngrx store not dispatching an action to the effect supposed to deal with it.
Here is the component that tries to dispatch:
signin() {
this.formStatus.submitted = true;
if (this.formStatus.form.valid) {
this.store.dispatch(new StandardSigninAction(this.formStatus.form.value.credentials));
}
}
The actions:
export const ActionTypes = {
STANDARD_SIGNIN: type('[Session] Standard Signin'),
LOAD_PERSONAL_INFO: type('[Session] Load Personal Info'),
LOAD_USER_ACCOUNT: type('[Session] Load User Account'),
RELOAD_PERSONAL_INFO: type('[Session] Reload Personal Info'),
CLEAR_USER_ACCOUNT: type('[Session] Clear User Account')
};
export class StandardSigninAction implements Action {
type = ActionTypes.STANDARD_SIGNIN;
constructor(public payload: Credentials) {
}
}
...
export type Actions
= StandardSigninAction
| LoadPersonalInfoAction
| ClearUserAccountAction
| ReloadPersonalInfoAction
| LoadUserAccountAction;
The effect:
#Effect()
standardSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.STANDARD_SIGNIN)
.map((action: StandardSigninAction) => action.payload)
.switchMap((credentials: Credentials) =>
this.sessionSigninService.signin(credentials)
.map(sessionToken => {
return new LoadPersonalInfoAction(sessionToken);
})
);
I can see in debug that the component does call the dispatch method. I can also confirm that StandardSigninAction is indeed instantiated because the breakpoint in the constructor is hit.
But the standardSignin$ effect is not called...
What can possibly cause an effect not being called?
How can I debug what is going on within the store?
Can someone please help?
P.S. I do run the above effect as follows in my imports:
EffectsModule.run(SessionEffects),
edit: Here is my SessionSigninService.signin method (does return an Observable)
signin(credentials: Credentials) {
const headers = new Headers({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'});
const options = new RequestOptions({headers: headers});
const body = 'username=' + credentials.username + '&password=' + credentials.password;
return this.http.post(this.urls.AUTHENTICATION.SIGNIN, body, options).map(res => res.headers.get('x-auth-token'));
}
This is not going to be a definitive answer, but, hopefully, it will be helpful.
Before you start:
Make sure you are using the latest versions of the #ngrx packages (that are appropriate for the version of Angular you are using).
If you've updated any packages, make sure you re-start your development environment (that is, restart the bundler, the server, etc.)
If you've not done so already, you should have a look at the implementation of the Store - so that you make some educated guesses as to what could be going wrong. Note is that the Store is pretty light. It's both an observable (using the state as its source) and an observer (that defers to the dispatcher).
If you look at store.dispatch you'll see that it's an alias for
store.next, which calls next on the Dispatcher.
So calling:
this.store.dispatch(new StandardSigninAction(this.formStatus.form.value.credentials));
should just see an action emitted from the dispatcher.
The Actions observable that's injected into your effects is also pretty light. It's just an observable that uses the Dispatcher as its source.
To look at the actions that are flowing through the effect, you could replace this:
#Effect()
standardSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.STANDARD_SIGNIN)
with this:
#Effect()
standardSignin$: Observable<Action> = this.actions$
.do((action) => console.log(`Received ${action.type}`))
.filter((action) => action.type === session.ActionTypes.STANDARD_SIGNIN)
ofType is not an operator; it's a method, so to add do-based logging, it needs to be replaced with a filter.
With the logging in place, if you are receiving the action, there is something wrong with the effect's implementation (or maybe the action types' strings/constants aren't what you think they are and something is mismatched).
If the effect is not receiving the dispatched action, the most likely explanation would be that the store through which you are dispatching the StandardSigninAction is not that same store that your effect is using - that is, you have a DI problem.
If that is the case, you should look at what differs from the other SessionEffects that you say are working. (At least you have something working, which is a good place to start experimenting.) Are they dispatched from a different module? Is the module that dispatches StandardSigninAction a feature module?
What happens if you hack one of the working SessionEffects to replace its dispatched action with StandardSigninAction? Does the effect then run?
Note that the questions at the end of this answer aren't questions that I want answered; they are questions that you should be asking yourself and investigating.
Your store's stream may be stopping because of either unhandled errors or - perhaps more confusingly - errors that seem 'handled' using .catch that actually kill the stream without re-emitting a new Observable to keep things going.
For example, this will kill the stream:
this.actions$
.ofType('FETCH')
.map(a => a.payload)
.switchMap(query => this.apiService.fetch$(query)
.map(result => ({ type: 'SUCCESS', payload: result }))
.catch(err => console.log(`oops: ${err}`))) // <- breaks stream!
But this will keep things alive:
this.actions$
.ofType('FETCH')
.map(a => a.payload)
.switchMap(query => this.apiService.fetch$(query)
.map(result => ({ type: 'SUCCESS', payload: result }))
.catch(e => Observable.of({ type: 'FAIL', payload: e}))) // re-emit
This is true for any rxjs Observable btw, which is especially important to consider when broadcasting to multiple observers (like ngrx store does internally using an internal Subject).
I am using a later version of ngrx (7.4.0), so cartant's suggestion of:
.do((action) => console.log(`Received ${action.type}`))
should be...
... = this.actions.pipe(
tap((action) => console.log(`Received ${action.type}`)),
...
And in the end I discovered I had missed adding my new effects export to module, like:
EffectsModule.forRoot([AuthEffects, ViewEffects]), // was missing the ', ViewEffects'
If you are using version 8, ensure you wrap each action with createEffect.
Example:
Create$ = createEffect(() => this.actions$.pipe(...))
Another possible reason is that if you used ng generate to create the module where you imported the Effects make sure it is imported in the App Module as the following command 'ng generate module myModule' will not add it to the app module.
I've got a pretty straight forward effect defined using the ngrx/effects library.
#Effect()
public Authorize$ = this._actions$.ofType(IdentityActionsService.AUTHORIZE_IDENTITY)
.switchMap(action => this._svc.Authorize$(action.payload))
.catch(err => Observable.of(null).do(() => console.error(err); }))
.map(identity => this._identity.OnIdentityAuthorized(identity))
The #Effect is triggered, authorize$() runs, and the OnIdentityAuthorized() method, which returns an Action ({type: payload: }) fires...
What I expect to happen is that the action returned by OnIdentityAuthorized() should get fed into the appropriate reducer - that is not happening.
I have a debugger call in OnIdentityAuthorized and in the corresponding reducer. The Action returned by OnIdentityAuthorized is not being dispatched. What might cause this? Am I misunderstanding something?
I feel like what I've got is basically identical to example 1 here: https://github.com/ngrx/effects/blob/master/docs/intro.md
EDIT
Added additional code sections... The effect triggers the OnIdentityAuthorized debugger statement, so the observable is emitting all the way through the async authorization call. The reducer case is not triggered...
Here is the OnIdentityAuthorized() implementation:
public static ON_IDENTITY_AUTHORIZED = '[IDENTITY] Authorized';
public OnIdentityAuthorized(identity: Identity | JWT): Action {
debugger;
return {
type: IdentityActionsService.ON_IDENTITY_AUTHORIZED,
payload: identity
};
}
Here is the reducer section:
switch (action.type) {
case IdentityActionsService.ON_IDENTITY_AUTHORIZED:
debugger;
return merge({}, action.payload);
Turns out to have been an issue with the way I was registering the reducers.
I was trying to do something exotic in order to add additional state endpoints for lazy loaded modules... turns out there is an issue there - but that is a different question.