Redux async call with then does not wait for api response - reactjs

I'm defining a Redux service to call an api endpoint:
export const TrackersApi = {
getBasicsTrackers: async (): Promise<ReturnType<typeof recreator>> => {
const url = "/api/getbasictrackers"
const {data, statusText} = await axios.get(url, { withCredentials: true });
if(statusText !== 'OK' && statusText !== 'No Content') throw new Error('Wrong response from getbasictrackers')
const result = recreator(data)
console.log({result})
return result
},
The log returns the json response.
Then I inject this in a component on mount:
componentDidMount = () => {
store.dispatch(getBasicTrackers()).then(() => {
if(this.props.trackers) {
this.setState({
sortedAndFilteredTrackers : this.props.trackers
})
}
if(this.props.folders) {
this.setState({
sortedAndFilteredFolders: this.props.folders
})
}
console.log('trackers', this.props.trackers)
})
}
However the log here returns an empty array. I tried first without the then and I had the same issue.
How can I make it so that the setState is called only once the API response is received?
Additional info: This props is then used to fill in a table. However the table remains empty, which is the key issue here
It is mapped through this:
const mapStateToProps = (state: ReduxStore.State) => ({
trackers: state.trackersData.rawTrackers ? Object.values(state.trackersData.rawTrackers).map(item => ({...item, checked: false})) : [],
folders: state.trackersData?.folders ? Object.values(state.trackersData.folders).map((folder: any) => ({...folder.summary, checked: false})) : []
})

Related

Re-Usable fetch function with query string

I have a fetch function inside of my react component, which I wish to "outsourse" in a separate component.
export const fetchBooksBySubject = (selectedValue) => {
const options = {
method: `GET`,
};
fetch(`${server}/books?subjects_like=${selectedValue}`, options)
.then((response) => {
if(response.ok){
return response.json()
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data: ', error)
})
}
Basically selectedValue is a prop coming from a child of App.jsx. As soon as the value is selected in a component, fetch should fire with this value in a query string. I tried to export the function above as a component and use it in App.jsx
useEffect(() => {
fetchBooksBySubject(selectedValue).then(data => setBookList(data));
}, [selectedValue])
const handleChange = e => {
setSelectedValue(e);
fetchBooksBySubject(selectedValue);
};
But this throws Property 'then' does not exist on type 'void'.
Here's a custom hook you can use with fast and reusable data fetching, a built-in cache, and other features like polling intervals and revalidation.
Hook:
const useBooks = (selectedValue) =>
{
const fetcher = (...args) => fetch(...args).then(res => res.json())
const { data, error } = useSWR(`/api/books?subjects_like=${selectedValue}`, fetcher)
return {
books: data,
isLoading: !error && !data,
isError: error
}
}
Usage:
const { books, isLoading, isError } = useBooks(selectedValue)
if (isLoading) return <div>Loading...</div>
else return <div>Your content here</div>
swr docs
Without swr:
useEffect(() =>
{
const fetchData = async (selectedValue) =>
{
const books = await fetchBookBySubject(selectedValue)
setBookList(books)
}
fetchData(selectedValue)
}, [selectedValue, bookList])
So the problem was, that I wasn't returning my fetch. I am a beginner, so my understanding is, that my App.js just couldn't access the data from fetchBooksBySubject withot this return
const dev = process.env.NODE_ENV !== 'production';
const server = dev ? 'http://localhost:3001' : 'https://your_deployment.server.com';
// later definable for developement, test, production
export const FetchBooksBySubject = (selectedValue) => {
const options = {
method: `GET`,
};
return fetch(`${server}/books?subjects_like=${selectedValue}`, options)
.then((response) => {
if(response.ok){
return response.json()
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data: ', error)
})
}
Same as here:
let sum = (a,b) => {a+b}
sum(1,2) //undefined
let sum1 = (a,b) => {return a+b}
sum1(1,2) //3

how to call reactQuery refetch in input onchange event in reactjs

In my React application, I need to call does exit API. API call should happen when change event in the input for that, I am using the reactQuery refetch to do that.
I had tried with below code
const [createObj, setCreateObj] = useState(mCreateObj);
const { data: doexit, refetch: doexitRefetch } = useQuery('getDoexit', () => api.doexitAPI(createObj.c_name), { enabled: false });
const handleInput = ({ target: { name, value } }) => { setCreateObj(state => ({ ...state, [name]: value }), []); }
export const doexitAPI= (value) => axios.get(/doexist/${value}, { headers: setHeader }).then(res => res);
useEffect(() => { console.log(createObj) doexitRefetch(); }, [createObj.mx_name])
How to call in input onchange event
You can invalidate your query and handle fetch data again with query keys.
https://react-query.tanstack.com/guides/query-keys#if-your-query-function-depends-on-a-variable-include-it-in-your-query-key
const { data: doexit, refetch: doexitRefetch } = useQuery(['getDoexit', createObj.mx_name], () => api.doexitAPI(createObj.c_name), { enabled: false });

accessing react state variable outside of a function in a component is printing null

I have a AutoHospitals component and I am trying to get the value of a state variable outside the .then function but it is printing null.
Here is the code snippet where this.state.retrievedmrnNumber is printing.
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
let retrievedMrnNo = response.data.mrnNumber;
this.setState({ retrievedmrnNumber: retrievedMrnNo});
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
Here is the console log statements outside the above .then function, where it is printing null:
console.log("Outside of then function: Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
How do I access it outside of .then function?My ultimate goal is to use the value on this line:
selectedHospitals = [{label: this.props.value[0] && this.state.retrievedmrnNumber || 'Select'}]
Full component code is below:
export class AutoHospitals extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
selectedHospitalValues: null,
selectedHospitals: [],
retrievedmrnNumber:null,
loading: false
};
this.onChange = this.onChange.bind(this);
}
onChange = (val) => {
this.setState({
value: val,
selectedHospitalValues: val
});
this.props.onChange(val)
};
fetchRecords() {
let url = 'myurl'
this.setState({
loading: true
});
return axios
.get(url)
.then(response => {
let selectedHospitals;
if(this.props.value[0]){
console.log('this.props.value is DEFINED - Request has been EDITED!!!!')
// START: Logic to get MRN Number
let hospitalIdtoRetrieveMRNNumber = this.props.value[0].hospitalId;
axios
.get('api/Hospitalses/'+hospitalIdtoRetrieveMRNNumber)
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
let retrievedMrnNo = response.data.mrnNumber;
this.setState({ retrievedmrnNumber: retrievedMrnNo});
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
// END: Logic to get mrn Number
console.log("Outside response block: Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
selectedHospitals = [{label: this.props.value[0] && this.state.retrievedmrnNumber || 'Select'}]
//let selectedHospitals = [{label: this.props.value[0] && 'mrn # 1234' || 'Select'}]
}else {
console.log('this.props.value is UNDEFINED - it is a NEW REQUEST');
}
this.setState({
loading: false
});
if (this.props.value) {
this.props.value.forEach(e => {
selectedHospitals.push(response.data._embedded.Hospitalses.filter(hospitalSet => {
return hospitalSet.hospitalId === e.hospitalId
})[0])
})
}
this.setState({
selectedHospitals: response.data._embedded.Hospitalses.map(item => ({
label: (item.mrnNumber.toString()),
projectTitle: item.projectTitle,
hospitalId: item.hospitalId,
})),
selectedHospitalsValues: selectedHospitals
});
}).catch(err => console.log(err));
}
componentDidMount() {
this.fetchRecords(0)
}
render() {
return (
<div>
<Hospitalselect value={this.state.selectedHospitalsValues} options={this.state.selectedHospitals} onChange={this.onChange } optionHeight={60} />
<div className="sweet-loading" style={{ marginTop: '-35px' }}>
<ClockLoader
css={override}
size={30}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
It's all about sync\async. Consider following two examples:
With then which is fully async (and do not allowing any waits) :
export const download = (url, filename) => {
fetch(url, {
mode: 'no-cors'
/*
* ALTERNATIVE MODE {
mode: 'cors'
}
*
*/
}).then((transfer) => {
return transfer.blob(); // RETURN DATA TRANSFERED AS BLOB
}).then((bytes) => {
let elm = document.createElement('a'); // CREATE A LINK ELEMENT IN DOM
elm.href = URL.createObjectURL(bytes); // SET LINK ELEMENTS CONTENTS
elm.setAttribute('download', filename); // SET ELEMENT CREATED 'ATTRIBUTE' TO DOWNLOAD, FILENAME PARAM AUTOMATICALLY
elm.click(); // TRIGGER ELEMENT TO DOWNLOAD
elm.remove();
}).catch((error) => {
console.log(error); // OUTPUT ERRORS, SUCH AS CORS WHEN TESTING NON LOCALLY
})
}
With await, where the response becomes sync:
export const download = async (url, filename) => {
let response = await fetch(url, {
mode: 'no-cors'
/*
* ALTERNATIVE MODE {
mode: 'cors'
}
*
*/
});
try {
let data = await response.blob();
let elm = document.createElement('a'); // CREATE A LINK ELEMENT IN DOM
elm.href = URL.createObjectURL(data); // SET LINK ELEMENTS CONTENTS
elm.setAttribute('download', filename); // SET ELEMENT CREATED 'ATTRIBUTE' TO DOWNLOAD, FILENAME PARAM AUTOMATICALLY
elm.click(); // TRIGGER ELEMENT TO DOWNLOAD
elm.remove();
}
catch(err) {
console.log(err);
}
}
The await example can be called as anonymous function (hope the normal call also possible):
(async () => {
await download('/api/hrreportbyhours',"Report "+getDDMMYYY(new Date())+".xlsx");
await setBtnLoad1(false);
})();
I believe the Promise from the async axios.get function hasn't resolved by the time you call the state value in selectedHospitals
Try passing a callback function to the return of the then statement:
.then(response => {
console.log("Extracting mrnNumber from Hospitals API results")
console.log(response.data.mrnNumber);
handleRequest(response.data.mrnNumber);
console.log("Printing Retrieved mrn number from state");
console.log(this.state.retrievedmrnNumber);
})
And here is the callback which can use setState:
handleRequest(data) {
this.setState({ retrievedmrnNumber: data});
}
EDIT To bind handle request to this properly try making it an arrow function:
handleRequest = (data) => this.setState({retrievedmrnNumber:data});

Best Practice for handling consecutive identical useFetch calls with React Hooks?

Here's the useFetch code I've constructed, which is very much based upon several well known articles on the subject:
const dataFetchReducer = (state: any, action: any) => {
let data, status, url;
if (action.payload && action.payload.config) {
({ data, status } = action.payload);
({ url } = action.payload.config);
}
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: data,
status: status,
url: url
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
data: null,
status: status,
url: url
};
default:
throw new Error();
}
}
/**
* GET data from endpoints using AWS Access Token
* #param {string} initialUrl The full path of the endpoint to query
* #param {JSON} initialData Used to initially populate 'data'
*/
export const useFetch = (initialUrl: ?string, initialData: any) => {
const [url, setUrl] = useState<?string>(initialUrl);
const { appStore } = useContext(AppContext);
console.log('useFetch: url = ', url);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
status: null,
url: url
});
useEffect(() => {
console.log('Starting useEffect in requests.useFetch', Date.now());
let didCancel = false;
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
// We won't force an error if there's no URL
if (!didCancel && url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url, appStore.awsConfig]);
return [state, setUrl];
}
This seems to work fine except for one use case:
Imagine a new Customer Name or UserName or Email Address is typed in - some piece of data that has to be checked to see if it already exists to ensure such things remain unique.
So, as an example, let's say the user enters "My Existing Company" as the Company Name and this company already exists. They enter the data and press Submit. The Click event of this button will be wired up such that an async request to an API Endpoint will be called - something like this: companyFetch('acct_mgmt/companies/name/My%20Existing%20Company')
There'll then be a useEffect construct in the component that will wait for the response to come back from the Endpoint. Such code might look like this:
useEffect(() => {
if (!companyName.isLoading && acctMgmtContext.companyName.length > 0) {
if (fleetName.status === 200) {
const errorMessage = 'This company name already exists in the system.';
updateValidationErrors(name, {type: 'fetch', message: errorMessage});
} else {
clearValidationError(name);
changeWizardIndex('+1');
}
}
}, [companyName.isLoading, companyName.isError, companyName.data]);
In this code just above, an error is shown if the Company Name exists. If it doesn't yet exist then the wizard this component resides in will advance forward. The key takeaway here is that all of the logic to handle the response is contained in the useEffect.
This all works fine unless the user enters the same Company Name twice in a row. In this particular case, the url dependency in the companyFetch instance of useFetch does not change and thus there is no new request sent to the API Endpoint.
I can think of several ways to try to solve this but they all seem like hacks. I'm thinking that others must have encountered this problem before and am curious how they solved it.
Not a specific answer to your question, more of another approach: You could always provide a function to trigger a refetch by the custom hook instead of relying of the useEffect to catch all different cases.
If you want to do that, use useCallback in your useFetch so you don't create an endless loop:
const triggerFetch = useCallback(async () => {
console.log('Starting useCallback in requests.useFetch', Date.now());
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
dispatch({ type: 'FETCH_SUCCESS', payload: response });
} catch (error) {
// We won't force an error if there's no URL
if (url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
}, [url, appStore.awsConfig]);
..and at the end of the hook:
return [state, setUrl, triggerFetch];
You can now use triggerRefetch() anywhere in your consuming component to programmatically refetch data instead of checking every case in the useEffect.
Here is a complete example:
CodeSandbox: useFetch with trigger
To me this slightly related to thing "how to force my browser to skip cache for particular resource" - I know, XHR is not cached, just similar case. There we may avoid cache by providing some random meaningless parameter in URL. So you can do the same.
const [requestIndex, incRequest] = useState(0);
...
const [data, updateURl] = useFetch(`${url}&random=${requestIndex}`);
const onSearchClick = useCallback(() => {
incRequest();
}, []);

React Hooks - How to test changes on global providers

I'm trying to test the following scenario:
A user with an expired token tries to access a resource he is not authorized
The resources returns a 401 error
The application updates a global state "isExpiredSession" to true
For this, I have 2 providers:
The authentication provider, with the global authentication state
The one responsible to fetch the resource
There are custom hooks for both, exposing shared logic of these components, i.e: fetchResource/expireSesssion
When the resource fetched returns a 401 status, it sets the isExpiredSession value in the authentication provider, through the sharing of a setState method.
AuthenticationContext.js
import React, { createContext, useState } from 'react';
const AuthenticationContext = createContext([{}, () => {}]);
const initialState = {
userInfo: null,
errorMessage: null,
isExpiredSession: false,
};
const AuthenticationProvider = ({ authStateTest, children }) => {
const [authState, setAuthState] = useState(initialState);
return (
<AuthenticationContext.Provider value={[authStateTest || authState, setAuthState]}>
{ children }
</AuthenticationContext.Provider>);
};
export { AuthenticationContext, AuthenticationProvider, initialState };
useAuthentication.js
import { AuthenticationContext, initialState } from './AuthenticationContext';
const useAuthentication = () => {
const [authState, setAuthState] = useContext(AuthenticationContext);
...
const expireSession = () => {
setAuthState({
...authState,
isExpiredSession: true,
});
};
...
return { expireSession };
}
ResourceContext.js is similar to the authentication, exposing a Provider
And the useResource.js has something like this:
const useResource = () => {
const [resourceState, setResourceState] = useContext(ResourceContext);
const [authState, setAuthState] = useContext(AuthenticationContext);
const { expireSession } = useAuthentication();
const getResource = () => {
const { values } = resourceState;
const { userInfo } = authState;
return MyService.fetchResource(userInfo.token)
.then((result) => {
if (result.ok) {
result.json()
.then((json) => {
setResourceState({
...resourceState,
values: json,
});
})
.catch((error) => {
setErrorMessage(`Error decoding response: ${error.message}`);
});
} else {
const errorMessage = result.status === 401 ?
'Your session is expired, please login again' :
'Error retrieving earnings';
setErrorMessage(errorMessage);
expireSession();
}
})
.catch((error) => {
setErrorMessage(error.message);
});
};
...
Then, on my tests, using react-hooks-testing-library I do the following:
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
// Here is the issue, how to test the global value of the Authentication context? the line below, of course, doesn't work
expect(result.current.isExpiredSession).toBeTruthy();
});
I have tried a few solutions:
Rendering the useAuthentication on the tests as well, however, the changes made by the Resource doesn't seem to reflect on it.
Exposing the isExpiredSession variable through the Resource hook, i.e:
return {
...
isExpiredSession: authState.isExpiredSession,
...
};
I was expecting that by then this line would work:
expect(result.current.isExpiredSession).toBeTruthy();
But still not working and the value is still false
Any idea how can I implement a solution for this problem?
Author of react-hooks-testing-library here.
It's a bit hard without being able to run the code, but I think your issue might be the multiple state updates not batching correctly as they are not wrapped in an act call. The ability to act on async calls is in an alpha release of react (v16.9.0-alpha.0) and we have an issue tracking it as well.
So there may be 2 ways to solve it:
Update to the alpha version and a move the waitForNextUpdate into the act callback
npm install react#16.9.0-alpha.0
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
await act(async () => {
result.current.getResource();
await waitForNextUpdate();
});
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Add in a second waitForNextUpdate call
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
// await setErrorMessage to happen
await waitForNextUpdate();
// await setAuthState to happen
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Your appetite for using alpha versions will likely dictate which option you go for, but, option 1 is the more "future proof". Option 2 may stop working one day once the alpha version hits a stable release.

Resources