So I have a function that fetches from an API until it hits a 200 code.
Something like this:
/* These functions are in a separate file */
const fetchFile = async () => {
try {
const resp = await fetch('someUrl');
return resp.data;
} catch (err) {
if(err.response.code === 404){
await sleep(10); //sleep is a custom fn that awaits for N seconds
return fetchFile();
}else{
return 'Error';
}
}
}
const fetchAll = async (setData, setError) => {
const data = await fetchFile();
if(data === 'Error') {
setError('Sorry, error ocurred');
return;
}
setData(data);
}
/* React component */
const [data, setData] = useState([]);
const [error, setError] = useState('');
<button onClick={fetchAll}>Start fetch</button>
And I need to have another button that let's the user stop the recursing function. I know a common way of doing this is having a flag and check the value every time I call the recursing function, but since this is React, how can I achieve this? Should I have the functions inside the component file? Use a global window variable? Use localstorage? What's the best way?
You can do couple of ways, here is one the way using useRef
/* These functions are in a separate file */
const fetchFile = async (cancellationRef) => {
try {
const resp = await fetch('someUrl');
return resp.data;
} catch (err) {
if(err.response.code === 404){
if(cancellationRef.current !==true){
await sleep(10); //sleep is a custom fn that awaits for N seconds
return fetchFile();
}
else{
return 'Request cancelled';
}
}else{
return 'Error';
}
}
}
const fetchAll = async (setData, setError,cancellationRef) => {
const data = await fetchFile(cancellationRef);
if(data === 'Error') {
setError('Sorry, error ocurred');
return;
}
setData(data);
}
/* React component */
const [data, setData] = useState([]);
const cancellationRef = useRef(false);
const [error, setError] = useState('');
<button onClick={fetchAll}>Start fetch</button>
<button onClick={()=>cancellationRef.current =true)}>Stop looping</button>
Here is a full example - I recommend encapsulating the fetching behavior in a hook like this. An AbortController is used for cancelling any ongoing request. This takes advantage of useEffect and its cleanup function, which is described in the react docs:
If your effect returns a function, React will run it when it is time to clean up:
/** Continuously makes requests until a non-404 response is received */
const fetchFile = async (url, signal) => {
try {
const resp = await fetch(url, { signal });
return resp.data;
} catch (err) {
if(err.response.code === 404){
await sleep(10);
return fetchFile(url, signal);
}
// there was an actual error - rethrow
throw err;
}
}
/** Hook for fetching data and cancelling the request */
const useDataFetcher = (isFetching) => {
// I advise using `null` as default values
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (isFetching) {
const controller = new AbortController();
fetchFile('someUrl', controller.signal)
.then(result => {
setData(result);
setError(null);
})
.catch(err => {
if (err.name === "AbortError") {
// request was aborted, reset state
setData(null);
setError(null);
} else {
setError(err);
}
});
// This cleanup is called every time the hook reruns (any time isFetching changes)
return () => {
if (!controller.signal.aborted) {
controller.abort();
}
}
}
}, [isFetching]);
return { data, error };
}
const MyComponent = () => {
const [isFetching, setIsFetching] = useState(false);
const { data, error } = useDataFetcher(isFetching);
const startFetch = useCallback(() => setIsFetching(true), []);
const cancelFetch = useCallback(() => setIsFetching(false), []);
useEffect(() => {
if (data || error) {
setIsFetching(false);
}
}, [data, error];
return isFetching
? <button onClick={cancelFetch}>Cancel</button>
: <button onClick={startFetch}>Start fetch</button>
}
Related
I am trying to make my own useFetch hook.
export const useFetch = <T extends unknown>(
url: string,
options?: RequestInit
) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [response, setResponse] = useState<T>();
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { ...options, signal: controller.signal })
.then((res) => {
if (!res.ok) {
setError(true);
} else {
setError(false);
}
return res.json();
})
.then((json) => {
setLoading(false);
setResponse((json as unknown as JSONResponse).content as T);
})
.catch((err) => {
console.log(err);
setLoading(false);
setError(true);
});
return () => {
controller.abort();
};
}, [url, options]);
return { loading, error, response };
};
The fetch runs every time the URL changes. My problem is that I use it like this in my Home.tsx
const [url, setUrl] = useState('')
const roomIDRef = useRef() as React.MutableRefObject<HTMLInputElement>;
const { error, response, loading } = useFetch<ExistsRoom>(
url
);
const joinRoom = async () => {
const id = roomIDRef.current.value;
if (id === '') {
return;
}
setUrl(() => `http://localhost:8808/api/v1/room/exists?id=${id}`);
};
The user has to input an id and press a button to run this fetch. It should check if the room with this id exists. My problem is if the user tries to check the room with the id=test and the request will be made he gets his response. But if he tries to press the button again, which will mean he will request the same id and therefore the same url it won't fetch again because the url doesn't change.
Can I work around this somehow?
I tried to add a random query parameter to the URL and it works
setUrl(() => `http://localhost:8808/api/v1/room/exists?id=${id}&t=${Date.now().toString()}`);
but I don't think this is the cleanest way to do it.
Add a counter and every time that use clicks the button again, raise the counter. Add counter to dependencies.
I have my App component defined as below;
function App() {
const [state1, setState1] = useState({});
const [state2, setState2] = useState({});
const [isApiCallDone, setIsApiCallDone] = useState(false);
const fetchFn = useMyCustomFetch();
useEffect(() => {
(async function() {
try {
let [state11] = await fetchFn('api/api1', {}, 'GET');
let [state22] = await fetchFn('api/api2', {}, 'GET');
setState1(state11); // Is there a better way to set this ?
setState2(state22);
setIsApiCallDone(true);
} catch (e) {
console.error(e);
}
})();
}, []);
useEffect(() => {
if (Object.keys(state1).length > 0 && Object.keys(state2).length > 0) {
// Set some other state variables on App
}
}, [state1, state2])
return (
<>
<MyContextProvider>
{isApiCallDone && (
<MyComponent />
)
}
</MyContextProvider>
</>
}
Also my useMyCustomFetch hook looks like below
export default function useMyCustomFetch() {
const fetchData = async (url, reqData, reqType) => {
try {
var statusObj = {
statMsg: null
};
const response = await fetch(url, reqOptions);
if (!response.ok) {
throw response;
}
statusObj.status = "success";
const json = await response.json();
return [json, statusObj];
} catch (error) {
statusObj.status = "error";
return [null, statusObj];
}
}
return fetchData;
}
My questions are;
For the lines
let [state11] = await fetchFn('api/api1', {}, 'GET');
setState1(state11);
I first assign it to a new variable state11 and then assign the same by calling setState1.
Is there a better way to set the state1 directly?
Is the usage of async function inside the useEffect fine ?
For Question 1:
You can directly setState like below without using state11.
useEffect(() => {
(async function () {
try {
setState1(
(await fetchFn("https://reqres.in/api/users/1", {}, "GET"))[0]
); // Is there a better way to set this ?
setState2(
(await fetchFn("https://reqres.in/api/users/2", {}, "GET"))[0]
);
setIsApiCallDone(true);
} catch (e) {
console.error(e);
}
})();
}, []);
For Question 2:
I don't see any problem using async & IIFE in the useEffect. In fact I like the way its done. Looks good to me.
Please find screenshot of the state being set properly in the console (I have used a dummy api url):
If you don't want to use async functions, you can use the Promise.prototype.then() method to combine your calls like this :
useEffect(() => {
fetchFn('api/api1', {}, 'GET').then(state => {
setState1(state[0]);
return fetchFn('api/api2', {}, 'GET')
}).then(state => {
setState2(state[0]);
setIsApiCallDone(true);
}).catch(console.log);
}, []);
An other way to set this with an async function but more factorised is this way :
useEffect(() => {
(async function() {
try {
await fetchFn('api/api1', {}, 'GET')
.then(tab => tab[0])
.then(setState1);
await fetchFn('api/api2', {}, 'GET');
.then(tab => tab[0])
.then(setState2);
setIsApiCallDone(true);
} catch (e) {
console.error(e);
}
})();
}, []);
Finally, the usage of the async function in an useEffect is not a problem.
my custom hooks :
import { useContext, useState } from "react";
import { AppContext } from "../context/app.context";
const usePostAxios = (url) => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const { setUrl, token } = useContext(AppContext);
const postData = async (data) => {
try {
setIsLoading(true);
axios.defaults.headers.common["X-Authreq"] = token;
await axios
.post(`${setUrl}/${url}`, data)
.then(({ data }) => {
setData(data);
})
.catch((err) => {
setError(err.response || err.request);
});
setIsLoading(false);
} catch (err) {
console.log(err);
}
};
return [isLoading, postData, data, error];
};
export { usePostAxios };
for posting data :
const [isPosting, postFun, res, err] = usePostAxios("/apidet/EmpLog");
const handleSubmit = async (val) => {
await postFun({
UserName: val.email,
password: val.pass,
});
if (res) {
const { Apd, Apt } = res.ResponseData;
dispatch({ type: "APP-LOGIN", token: `${Apd}:${Apt}` });
setIsError(false);
navigate("Home");
return;
} else {
setIsError(true);
return;
}
};
then why my code is getting in else block and getting error but when i press submit button again the code run successfully and my login process success
so why is my code not running on first try ?
am i doing something stupid ?
Well, wrote in this way, basically, when this this code is excecuted
if(res) {
// Something to do with result
}
the variable res has null as value, initially.
You should check for changes in res with useEffect instead, like:
useEffect(()=> {
if(res) {
// Something to do with result
}
}, [res])
New to React Hooks and unsure how to solve. I have the following snippet of code within my App.js file below.
What I am basically trying to achieve is to get the user logged in by calling the getUser() function and once I have the user id, then check if they are an authorised user by calling the function checkUserAccess() for user id.
Based on results within the the validIds array, I check to see if it's true or false and set authorised state to true or false via the setAuthorised() call.
My problem is, I need this to process first prior to performing my first render within my App.js file.
At the moment, it's saying that I'm not authroised even though I am.
Can anyone pls assist with what I am doing wrong as I need to ensure that authorised useState is set correctly prior to first component render of application, i.e. path="/"
const [theId, setTheId] = useState('');
const [authorised, setAuthorised] = useState(false);
const checkUserAccess = async (empid) => {
try {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
if (isAuthorised) {
setAuthorised(true)
} else {
setAuthorised(false)
}
} catch (err) {
console.error(err.message);
}
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
setTheId(theId);
checkUserAccess(theId);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
Unless you are wanting to partially render when you get the user ID, and then get the access level. There is no reason to have multiple useState's / useEffect's.
Just get your user and then get your access level and use that.
Below is an example.
const [user, setUser] = useState(null);
const checkUserAccess = async (empid) => {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
return isAuthorised;
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
const access = await checkUserAccess(theId);
setUser({
theId,
access
});
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!user) return <div>Loading</div>;
return <>{user.theId}</>
This way it should work
but keep in mind that you must render your app only if theId in the state is present, which will mean your user is properly fetched.
const [state, setState] = useState({ theId: '', isAutorized: false })
const getUser = async () => {
try {
const idResp = await fetch("http://localhost:4200/get-user");
const theId = await idResp.json();
const authResp = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await authResp.response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(theId);
setState({ theId, isAuthorised })
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!state.theId) return <div>Loading</div>;
if (state.theId && !isAuthorized) return <AccessNotAllowed />
return <Home />
So I have a fetch function like follow which I found on the net and seems like a great way to retrieve api information:
const useFetch = (url, options = defaultOptions) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
setLoading(false);
} catch (err) {
setError(err);
}
}, [options, url]);
useEffect(() => {
if (url) {
fetchData();
}
}, [url]);
return {
error,
fetchData,
loading,
response,
};
};
Now I try to retrieve the data in another component and update the view whenever the loading variable is updated.
Like so:
const Test = () => {
const { loading, response } = useFunctionThatExtendsUseFetch();
const template = () => {
if (loading) {
console.log('loading', loading);
return 'loading';
}
console.log('finished loading', response);
return response;
};
return (
<div>
<p>{content}</p>
</div>
);
};
But nothing budges, not sure why... The loading variable does change though when I console log it's value in the component.
Thank you
Unless I'm missing something, content is not defined and template is never used. Try this:
const Test = () => {
const { loading, response } = useFunctionThatExtendsUseFetch();
if (loading) {
console.log('loading', loading);
} else {
console.log('finished loading', response);
}
const content = loading ? 'loading' : response;
return (
<div>
<p>{content}</p>
</div>
);
};
Or you could simply replace {content} with {template()} in your code.
But if your response isn't a string, you'll have issues.