Dispatch actions from a custom hook using useQuery - react-hooks

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;
}

Related

using Fomik hook `useField` and `react-data-table-component` causes infinite loop

I am using react-data-table-component inside formik form to update new values. But the problem is whenever the MyTable component is re-rendered, the selectableRowSelected() callback is called, which triggers onSelectedRowsChange event in which I use helpers.setValue() to set value, which then makes MyTable component renders again. This whole process causes infinite loop, and I still don't have a solution for this.
function MyTable() {
const [data, setData] = useState([]);
const [field, meta, helpers] = useField({ name: "use" });
useEffect(() => {
fetch("https://reqres.in/api/users?page=1&per_page=3")
.then((res) => res.json())
.then((res) => setData(res.data));
}, []);
const handleChange = React.useCallback(({ selectedRows }) => {
let selectedIds = selectedRows.map(function (row) {
return parseInt(row.id);
});
selectedIds.sort();
console.log("🚀 ~ selectedIds", selectedIds);
// helpers.setValue(selectedIds, true); --> uncomment this will cause infinite loop.
}, []);
return (
<DataTable
title="User List"
columns={columns}
data={data}
selectableRows
selectableRowsHighlight
onSelectedRowsChange={handleChange}
selectableRowSelected={(row) => {
return meta.value.includes(parseInt(row.id));
}}
/>
);
}
CodeSandbox: https://codesandbox.io/s/goofy-tu-l1pxvb?file=/src/MyTable.jsx:375-1249
I think I've figured it out myself. But I will post it here in case anyone may encounter the same.
There is no way to stop this problem as the way RDT works is whenever selectableRowSelected is called, it will dispatch an action with type: SELECT_MULTIPLE_ROWS:
dispatch({
type: 'SELECT_MULTIPLE_ROWS',...
});
then in tableReducer, it returns toggleOnSelectedRowsChange as boolean value:
case 'SELECT_MULTIPLE_ROWS': {
...
return {
...state,
...,
toggleOnSelectedRowsChange,
};
which controls the trigger of onSelectedRowsChange event (as mentioned at comment in the source code):
useDidUpdateEffect(() => {
onSelectedRowsChange({ allSelected, selectedCount, selectedRows: selectedRows.slice(0) });
// onSelectedRowsChange trigger is controlled by toggleOnSelectedRowsChange state
}, [toggleOnSelectedRowsChange]);
Overall, solution for this problem is don't use formik with RDT for row selection, use another datatable lib.

Complete Function Before Remote Operation in NgRx

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.

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.

Can the completion of one async call be sequenced before the start of another using useEffect?

I'm trying to use useEffect in my React app but also refactor things more modularly. Shown below is the heart of actual working code. It resides in a Context Provider file and does the following:
1. Calls AWS Amplify to get the latest Auth Access Token.
2. Uses this token, in the form of an Authorization header, when an Axios GET call is made to an API Endpoint.
This works fine but I thought it would make more sense to move Step #1 into its own useEffect construct above. Furthermore, in doing so, I could then also store the header object as its own Context property, which the GET call could then reference.
Unfortunately, I can now see from console log statements that when the GET call starts, the Auth Access Token has not yet been retrieved. So the refactoring attempt fails.
useEffect(() => {
const fetchData = async () => {
const config = {
headers: { "Authorization":
await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert('Error getting authorization token: '.concat(error))
})
}};
await axios.get('http://127.0.0.1:5000/some_path', config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert('Error getting data from endpoint: '.concat(error));
});
};
fetchData();
}, [myContextObject.some_data]);
Is there a way of refactoring my code into two useEffect instances such that the first one will complete before the second one starts?
You could hold the config object in a state. This way you can separate both fetch calls and trigger the second one once the first one finished:
const MyComponent = props => {
const myContextObject = useContext(myContext);
const [config, setConfig] = useState(null);
useEffect(() => {
const fetchData = async () => {
const config = {
headers: {
Authorization: await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert("Error getting authorization token: ".concat(error));
})
}
};
setConfig(config);
};
fetchData();
}, [myContextObject.some_data]);
useEffect(() => {
if (!config) {
return;
}
const fetchData = async () => {
await axios
.get("http://127.0.0.1:5000/some_path", config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert("Error getting data from endpoint: ".concat(error));
});
};
fetchData();
// This should work for the first call (not tested) as it goes from null to object.
// If you need subsequent changes then youll have to track some property
// of the object or similar
}, [config]);
return null;
};

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