Complete Function Before Remote Operation in NgRx - rxjs

I'm having an issue with a race condition in NgRx. In the example below, I'm asynchronously presenting a loading dialog at about the same time as I'm starting an async remote operation. But the remote operation has the potential to complete and fire dismissLoadingDialog() before the loading dialog is fully built, which results in a console error.
What might be a good strategy in NgRx to complete presentLoadingDialog() before the remote operation begins?
#Effect() fetchServerData$ = this.actions$.pipe(
ofType<FetchServerData>(ActionTypes.FetchServerData),
switchMap(action => {
this.presentLoadingDialog('...loading');
return this.dataService.fetchData(action.payload).pipe(
map(result => {
this.dismissLoadingDialog();
return new FetchServerDataSuccess(result);
}),
catchError(err => of(new FetchServerDataFail(err)))
);
})
);
async presentLoadingDialog(message: string): Promise<void> {
this.isLoading = true;
return this.loadingCtrl
.create({
duration: 5000,
message: message
})
.then(loadingDialog => {
loadingDialog.present().then(() => {
if (!this.isLoading) {
loadingDialog.dismiss();
}
});
});
}
async dismissLoadingDialog() {
this.isLoading = false;
if (!isNullOrUndefined(this.loadingCtrl)): Promise<boolean> {
return this.loadingCtrl.dismiss();
}
}

Ionic's LoadingController create method returns a Promise which resolves when loader creation is complete. You can therefore use it in your effect's Observable chain:
presentLoadingDialog(message: string) {
const loader = this.loadingCtrl
.create({
duration: 5000,
message: message
});
return loader.present();
}
dismissLoadingDialog() {
this.loadingCtrl.dismiss();
}
#Effect() fetchServerData$ = this.actions$.pipe(
ofType<FetchServerData>(ActionTypes.FetchServerData),
switchMap(action => forkJoin(from(this.presentLoadingDialog('...loading'), of(action)),
switchMap(([_, action]) => this.dataService.fetchData(action.payload).pipe(
tap(() => this.dismissLoadingDialog()),
map(result => new FetchServerDataSuccess(result)),
catchError(err => {
this.dismissLoadingDialog();
return of(new FetchServerDataFail(err))
})
))
);

The standard I have seen is you have loading and loaded flags in your state. When you dispatch a load action the reducer updates the state with loading: true and loaded: false before the action fires the http request. The action then switch maps to an action that updates the state with the response and loading: false and loaded: true.
In your component you then have a selector for the loading flag and subscribe to it to open and close the dialog
this.loadingSub = loadings$.subscribe(loading => {
if (loading) {
this.presentLoadingDialog('...loading');
} else {
this.loadingDialog.dismiss();
}
});
unsubscribe in onDestroy
It should be up to your components to show UI components, I think actions calling loading dialogs is not an action concern. Tapping into the heart of state management to call UI components is not a pattern I would recommend.

Related

Dispatch actions from a custom hook using useQuery

I'm trying to write a custom hook that uses useQuery from react-query. The custom hook takes in the id of an employee and fetches some data and returns it to the consuming component. I want to be able to dispatch a redux action to show a loading indicator or show an error message if it fails. Here is my custom hook.
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
}
const query = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
})
if (query.isFetching || query.isLoading) {
dispatch(setWaiting())
}
return query.data
}
When I refresh the page, I get this error in the browser's console and I'm not sure how to fix this error?
Warning: Cannot update a component (`WaitIndicator`) while rendering a different component (`About`).
To locate the bad setState() call inside `About`, follow the stack trace as described in
The issue is likely with dispatching the setWaiting action outside any component lifecycle, i.e. useEffect. Move the dispatch logic into a useEffect hook with appropriate dependency.
Example:
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
};
const { data, isFetching, isLoading } = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
});
useEffect(() => {
if (isFetching || isLoading) {
dispatch(setWaiting());
}
}, [isFetching, isLoading]);
return data;
}

Get vuex store state after dispatching an action

I'm creating a chat application in Laravel 6 + Vue + Vuex. I want make a call to vuex store and get a state after a dispatch actions is complete and then I want to do some processing on that state in my vue component.
In ChatWindow component
mounted: function () {
this.$store.dispatch('setContacts').then(() => {
console.log('dispatch called')
// I want to call the getter here and set one of the data property
});
}
action.js
setContacts: (context) => {
axios.post('/users').then(response => {
let users = response.data;
// consoled for testing
console.log(users);
context.commit('setContacts', users);
});
}
mutators.js
setContacts: (state, users) => {
state.contacts = users;
},
Please see the screenshot below. The then method of dispatch is running before setContacts in action.js.
I need to call the getter after completing dispatch action. (which will effectively set the contacts state). Then, I want to get the contacts through getContacts getter like this.
getters.js
getContacts: (state) => {
return state.contacts;
}
I also tried calling computed property in then in mounted and it didn't work. Also, shouldn't 'dispatch called' in mounted run after console.log of setContacts in action.js as it is in then method? Thanks!
Maybe you could wrap axios call inside another promise.
return new Promise((resolve, reject) => {
axios.post('/users')
.then(response => {
let users = response.data;
// consoled for testing
console.log(users);
context.commit('setContacts', users);
resolve('Success')
})
.catch(error => {
reject(error)
})
})
And then
this.$store.dispatch('setContacts')
.then(() => {
console.log('dispatch called')
console.log('getter ', this.$store.getters.contacts)
});
Let me know what happens. It was working for a small demo that I tried.

Return axios Promise through Vuex

all!
I need to get axios Promise reject in my vue component using vuex.
I have serviceApi.js file:
export default {
postAddService(service) {
return axios.post('api/services', service);
}
}
my action in vuex:
actions: {
addService(state, service) {
state.commit('setServiceLoadStatus', 1);
ServicesAPI.postAddService(service)
.then( ({data}) => {
state.commit('setServiceLoadStatus', 2);
})
.catch(({response}) => {
state.commit('setServiceLoadStatus', 2);
console.log(response.data.message);
return Promise.reject(response); // <= can't catch this one
});
}
}
and in my vue component:
methods: {
addService() {
this.$store.dispatch('addService', this.service)
.then(() => {
this.forceLeave = true;
this.$router.push({name: 'services'});
this.$store.dispatch('snackbar/fire', {
text: 'New Service has been added',
color: 'success'
}).then()
})
.catch((err) => { // <== This never hapens
this.$store.dispatch('snackbar/fire', {
text: err.response.data.message || err.response.data.error,
color: 'error'
}).then();
});
}
When i use axios directly in my component all work well. I get both success and error messages.
But when i work using vuex i can't get error message in component, hoever in vuex action console.log prints what i need.
I'm always getting only successfull messages, even when bad things hapen on beckend.
How can i handle this situation using vuex ?
Wellcome to stackoverflow. One should not want to expect anything back from an action. After calling an action. Any response should be set/saved in the state via mutations. So rather have an error property on your state. Something like this should work
actions: {
async addService(state, service) {
try {
state.commit('setServiceLoadStatus', 1);
const result = await ServicesAPI.postAddService(service);
state.commit('setServiceLoadStatus', 2);
} catch (error) {
state.commit("error", "Could not add service");
state.commit('setServiceLoadStatus', 2);
console.log(response.data.message);
}
}
}
And in your component you can just have an alert that listens on your state.error
Edit: Only time you are going expect something back from an action is if you want to call other actions synchronously using async /await. In that case the result would be a Promise.

Wait for multiple actions to finish in epic before dispatching action

I have a component where I dispatch multiple actions:
dispatch(action1());
dispatch(action2());
I have an epic which listens for both of those actions, and makes an ajax call for each:
export const fetchObservable = action$ =>
action$.ofType(...actions) // wait for either action
.mergeMap(({ url, method, body, success, error, responseType }) =>
authenticate$
.mergeMap(token => ajax$(url, method, body, token, responseType))
.pluck('response')
.mergeMap(response => Observable.of(success(response), hideLoading()))
.startWith(showLoading())
.catch(response => Observable.of(error(response)))
);
I show a loading spinner with showLoading() and hide it with hideLoading()
The problem is that as soon as the first observable completes and Observable.of(success(response), hideLoading()) is called, the loading spinner disappears.
The reducer for the loader looks like this:
const loader = (state = false, action) => {
switch (action.type) {
case SHOW_LOADING:
return true;
case HIDE_LOADING:
return false;
default:
return state;
}
}
How can I wait for both actions to finish before calling hideLoading? How would I develop a solution that waits for x number of dispatches in a component?

Set State in Ajax Call Back throws error: Warning: setState(...): Can only update a mounted or mounting

I've got a fairly simple react container component that attempts to call set state in an ajax callback called from componentDidMount. The full error is:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the UserListContainer component.
the order of operations from my console.log are:
render
child-render
componentDidMount
ajax-data
[Big ol Error Message]
I started out using async/await but when I received the error I went back to callbacks with the same result. This is the relevant code:
export class UserListContainer extends React.Component<any, any>
{
constructor() {
super();
this.state = {
users: [], request: {}
};
}
//componentDidMount = async () => {
componentWillMount = () => {
console.log('componentWillMount');
//var response: Models.IUserQueryResponse = await Api.UserList.get(this.state.request);
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk) {
this.setState({ users: response.data, request: response.state });
}
});
}
render() {
console.log('render');
return <UserList
request={this.state.request}
users={this.state.users}
onEditClick={this.edit}
onRefresh={this.refresh}
/>;
}
Any help would be appreciated.
you cannot set state in componentWillMount because your component could be in a transitioning state.. also it will not trigger a re-rendering. Either use componentWillReceiveProps or componentDidUpdate.
Now that aside your issue is that you are calling setState in the callback from an API request. and the issue with that is you probably have unmounted that component and dont want to setState anymore.
you can fix this with a simple flag
constructor() {
super();
this.state = {
users: [], request: {}
};
this.isMounted = false;
}
componentDidMount(){
this.isMounted = true
}
componentWillUnmount(){
this.isMounted = false;
}
then in your api request you would do this.
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk && this.isMounted) {
this.setState({ users: response.data, request: response.state });
}
});
I think is better to use componentWillMount() instead of componentDidMount() cause you want to load the list and then set the state, not after the component was mounted.

Resources