I'm trying to use the hook useAPI that was describe in the first answer in useApi hook with multiple parameters
But I have a main problem. In my component I render the useAPI when in the url itself I have paramenters that might not have a value yet, which cause an error. The values comes from a different API call. How can I render my useAPI hook with the params only when they are defined?
my code looks like this:
const Availability = () => {
[account, setAccount] = useState(0);
const savedApiData = useAPI({
endpoint:'/programs/${state.account.id}/', // The account obj comes from a different useAPI in this component
requestType: 'POST',
body: {
siteIds: !state.siteId ? [] : [state.siteId]
}
});
}
On the first render the API url will be an error that says "cannot read property "id" of undefined".
This is really simple, in the fourth line, you're accessing the id property of state.account, but account here is undefined (probably because it doesn't exist). To solve the problem, you could use a dummy id when state.account is undefined (but you will definitely end up with a 404 from the server, so you must handle that), or don't use useApi at all. Hooks are really hard to tinker with, and sometimes a small unhandled use case for a hook can make it completely useless.
Another idea could be to allow the endpoint hook config parameter to be a function (only invoked when requesting), and add another boolean parameter to this object to control wether or not the request should be done.
Then, if state.account is undefined, no request is done, the endpoint function is never executed and no error occur, and when state.account has a value, the hook could re-try to launch the request, call the endpoint function and this time you get no error.
Example (not tested):
const useApi = ({endpoint, requestType, body, doRequest = true}) => {
const [data, setData] = useState({ fetchedData: [], isError: false, isFetchingData: false });
useEffect(() => {
if (doRequest) requestApi();
}, [endpoint, doRequest]); // Retry request when doRequest's value changes
const requestApi = async () => {
// invoke endpoint if it is a function
let endpointUrl = typeof endpoint === 'function' ? endpoint() : endpoint;
let response = {};
try {
setData({ ...data, isFetchingData: true });
console.log(endpointUrl);
switch (requestType) {
case 'GET':
return (response = await axios.get(endpointUrl));
case 'POST':
return (response = await axios.post(endpointUrl, body));
case 'DELETE':
return (response = await axios.delete(endpointUrl));
case 'UPDATE':
return (response = await axios.put(endpointUrl, body));
case 'PATCH':
return (response = await axios.patch(endpointUrl, body));
default:
return (response = await axios.get(endpoint));
}
} catch (e) {
console.error(e);
setData({ ...data, isError: true });
} finally {
if (response.data) {
setData({ ...data, isFetchingData: false, fetchedData: response.data.mainData });
}
}
};
return data;
};
Usage:
const Availability = () => {
[account, setAccount] = useState(0);
const savedApiData = useAPI({
endpoint: () => '/programs/${state.account.id}/', // Replace string literal with a factory function
requestType: 'POST',
body: {
siteIds: !state.siteId ? [] : [state.siteId]
},
doRequest: !!state.account, // Only request if account exists
});
}
Related
I developed login function use by react-query in my react app
The logic is as follows
First restAPI for checking duplication Email
If response data is true, send Second restAPI for sign up.
In this case, I try this way
// to declare useMutation object
let loginQuery = useMutation(checkDuple,{
// after check duplication i'm going to sign up
onSuccess : (res) => {
if(res === false && warning.current !== null){
warning.current.innerText = "Sorry, This mail is duplicatied"
}else{
let res = await signUp()
}
}
})
//---------------------------------------------------------------------------------
const checkDuple = async() => {
let duple = await axios.post("http://localhost:8080/join/duple",{
id : id,
})
}
const signUp = async() => {
let res = await axios.post("http://localhost:8080/join/signUp",{
id : id,
pass : pass
})
console.log(res.data)
localStorage.setItem("token", res.data)
navigate("/todo")
}
I think, this isn't the best way, If you know of a better way than this, please advise.
Better to have another async function that does both things.
something like
const checkDupleAndSignUp = async () => {
await checkDuple();
await signUp();
};
And then use that in your useMutation instead.
Other things to consider:
Maybe move the logic to set local storage and navigate to another page in the onSuccess instead.
You can also throw your own error if one or the other request fails and then check which error happened using onError lifecycle of useMutation, and maybe display a message for the user depending on which request failed.
You can handle both of them in a single function and in mutation just add token in localStorage and navigate
like this:
const checkDupleAndSignUp = async () => {
return checkDuple().then(async res => {
if (res === false) {
return {
isSuccess: false,
message: 'Sorry, This mail is duplicatied',
};
}
const { data } = await signUp();
return {
isSuccess: true,
data,
};
});
};
and in mutation :
let loginQuery = useMutation(checkDupleAndSignUp, {
onSuccess: res => {
if (res.isSuccess) {
console.log(res.data);
localStorage.setItem('token', res.data);
navigate('/todo');
} else if (warning.current !== null) {
warning.current.innerText = res.message;
}
},
});
My React app has an api client component which handles axios calls to the back end. So, in the api component I have:
async function getData(path: string, params?: any) {
const object: AxiosRequestConfig = {
...obj,
method: 'GET',
headers: {
...obj.headers,
},
params,
};
const response: AxiosResponse = await axios.get(
`${baseUrl}${path}`,
object
);
return response;
});
}
(obj contains the headers and is defined earlier in the file; baseUrl is a constant which I import).
So, if I have a useEffect to retrieve data from the endpoint '/user/{userId}' whenever the state variable userId changes, I do this:
React.useEffect(() => {
const controller = new AbortController();
const getData = async () => {
try {
const url = `user/${userId}`;
let res = await Client.getData(url, {
signal: controller.signal,
});
... Do things with results ...
} catch (e) {
// Show error
if (!controller.signal.aborted) console.log('Error: ', e);
}
};
getData();
return () => {
controller.abort();
};
}, [state.userId]);
I'm just a bit confused about how errors will be handled in this code. So, if there's an error when the axios call is made (eg no network connection, the endpoint is wrong, or the user isn't found or whatever) will the catch block get called in the getData function? Or do I need a try...catch in the api component too?
I am using a promise based hook in a React app to fetch async data from an API.
I am also using a Axios, a promise based http client to call the API.
Is it an anti-pattern to use a promise based client inside another promise? The below code does not seem to work.
const getData = () => {
return new Promise((resolve, reject) => {
const url = "/getData";
axios.get(url)
.then(function(response) {
resolve(response);
})
.catch(function(error) {
reject(error);
});
});
const useAsync = (asyncFunction) => {
const [value, setValue] = useState(null);
const execute = useCallback(() => {
setPending(true);
setValue(null);
setError(null);
return asyncFunction()
.then(response => setValue(response))
.catch(error => setError(error))
.finally(() => setPending(false));
}, [asyncFunction]);
useEffect(() => {
execute();
}, [execute]);
return { execute, pending, value, error };
};
};
const RidesList = () => {
const {
pending,
value,
error,
} = useAsync(getData);
Oh man. I think you have a fundamental misunderstanding about how Promises work.
First, axios already returns a Promise by default. So your whole first function of getData can be reduced to:
const getData = () => {
const url = "/getData"
return axios.get(url)
}
But the meat of your code seems to indicate you want a querable Promise - so you can check the status of it for whatever reason. Here's an example of how you would do it, adapted from this snippet:
function statusPromiseMaker(promise) {
if (promise.isResolved) return promise
let status = {
pending: true,
rejected: false,
fulfilled: false
}
let result = promise.then(
resolvedValue => {
status.fulfilled = true
return resolvedValue
},
rejectedError => {
status.rejected = true
throw rejectedError
}
)
.finally(() => {
status.pending = false
})
result.status = () => status
return result
}
In this way, you can then do something like let thing = statusPromiseMaker(getData()) and if you look up thing.status.pending you'll get true or false etc...
I didn't actually run what's above, I may have forgotten a bracket or two, but hopefully this helps.
I have to admit - I haven't seen anything like this ever used in the wild. I am interested in knowing what you're actually trying to accomplish by this.
Axios itself returns a promise but if you want to make a custom class having your custom logic after each API call then you can use interceptors I was having the same requirement and this is how I am returning promises after applying my custom logic on each API call.
Interceptors will get executed separately after and before each request you made so we can simply use them if we want to modify our request or response.
here is my working solution have a look at it.
callApi = (method, endpoint, params) => {
this.apiHandler.interceptors.request.use((config) => {
config.method = method
config.url = config.baseURL + endpoint
config.params = params
return config
})
return new Promise((resolve, reject) => {
this.apiHandler.interceptors.response.use((config) => {
if (config.status == 200) {
resolve(config.data)
} else {
reject(config.status)
}
// return config
}, error => reject(error))
this.apiHandler()
})
}
Below is the code to call this function
helper.callApi("get", "wo/getAllWorkOrders").then(d => {
console.log(d)
})
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();
}, []);
I created a custom react hook that is supposed to handle all less important api requests, which i don't want to store in the redux state. Hook works fine but I have trouble testing it. My test setup is jest and enzyme, but I decided to give a try react-hooks-testing-library here as well.
What I have tried so far is to first mock fetch request with a fetch-mock library, what works fine. Next, i render hook with renderHook method, which comes from react-hooks-testing-library. Unfortunately, looks like I do not quite understand the waitForNextUpdate method.
This is how my hook looks like.
useApi hook
export function useApi<R, B = undefined>(
path: string,
body: B | undefined = undefined,
method: HttpMethod = HttpMethod.GET
): ResponseStatus<R> {
const [response, setResponse] = useState();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | boolean>(false);
useEffect(() => {
const fetchData = async (): Promise<void> => {
setError(false);
setIsLoading(true);
try {
const result = await callApi(method, path, body);
setResponse(result);
} catch (errorResponse) {
setError(errorResponse);
}
setIsLoading(false);
};
fetchData();
}, [path, body, method]);
return { response, isLoading, error };
}
Hook can take 3 different state combinations that I would like to test. Unfortunately, I have no idea how.
Loading data:
{ response: undefined, isLoading: true, error: false }
Loaded data:
{ response: R, isLoading: false, error: false }
Error:
{ response: undefined, isLoading: false, error: true }
This is how my test looks like at this moment:
import fetchMock from 'fetch-mock';
import { useApi } from './hooks';
import { renderHook } from '#testing-library/react-hooks';
test('', async () => {
fetchMock.mock('*', {
returnedData: 'foo'
});
const { result, waitForNextUpdate } = renderHook(() => useApi('/data-owners'));
console.log(result.current);
await waitForNextUpdate();
console.log(result.current);
});
callApi func
/**
* Method to call api.
*
* #param {HttpMethod} method - Request type.
* #param {string} path - Restful endpoint route.
* #param {any} body - Request body data.
*/
export const callApi = async (method: HttpMethod, path: string, body: any = null) => {
// Sends api request
const response = await sendRequest(method, path, body);
// Checks response and parse to the object
const resJson = response ? await response.json() : '';
if (resJson.error) {
// If message contains error, it will throw new Error with code passed in status property (e.g. 401)
throw new Error(resJson.status);
} else {
// Else returns response
return resJson;
}
};
It's all right that you did. Now, you need use expect for test.
const value = {...}
expect(result.current).toEqual(value)