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.
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.
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>
}
I create a hook that manages the state of a single object with fetch to api. This hook exposes function to interact with this object.
// the hook
const useMyHook = () => {
const [myObject, setMyObject] = useState(null);
useEffect(() => {
const fetchData = async () => {
const data = await fetchSomething();
setMyObject(data);
}
fetchData();
}, []);
const updateMyObject = async () => {
console.log(myObject); // log : { ... }
try {
console.log(myObject); // log : undefined
// ...
} catch(err) {
// ...
}
};
return {
updateMyObject,
myObject
};
};
Then i use this hook inside a component and trigger updateMyObject() with a button.
// the component
const MyComponent = () => {
const { myObject, updateMyObject } = useMyHook();
return (
<button onClick={updateMyObject}>
Click me
</button>
);
};
How is this possible that before the try catch block the log is clean and inside the block i get undefined ?
I think this gonna work
useEffect(() => {
const fetchData = async () => {
const data = await fetchSomething();
setMyObject(data);
}
If(!myObject)
fetchData();
}, [myObject]);
Your code is perfectly alright !! There could be a problem in the fetchSomething() method. Ideally, it should return data, but it's not doing the same job.
Here is a small example. You can give it a try.
const fetchSomething = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
).then((res) => res.json());
return response;
};
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])
I'm making a get request to an API in a useEffect(). When I navigate to the page from the homepage it loads fine, but as soon as i refresh the page http://localhost:3000/coins/coin I get a Unhandled Runtime Error: Error: Network Error.
export async function getServerSideProps({ query }) {
const id = query;
return {
props: { data: id },
};
}
function index({ data }) {
const coinURL = data.id; // bitcoin
const apiEndpoint = `https://api.coingecko.com/api/v3/coins/${coinURL}`;
const [currentUser, setCurrentUser] = useState();
const [coinData, setCoinData] = useState([]);
useEffect(() => {
const getData = async () => {
const res = await axios.get(apiEndpoint);
const { data } = res;
setCoinData(data);
};
const getCurrentUser = async () => {
const res = await axios.get(
`http://localhost:5000/api/users/${session?.id}`
);
const { data } = res;
setCurrentUser(data);
};
getData();
getCurrentUser();
}, [coinData, currentUser]);
}
Why does this happen?
I'm recommending to do something like this:
const getData = async () => {
try {
const res = await axios.get(apiEndpoint);
const { data } = res;
setCoinData(data);
} catch(err) {
console.log(err)
}
};
const getCurrentUser = async () => {
try {
const res = await axios.get(
`http://localhost:5000/api/users/${session?.id}`
);
const { data } = res;
setCurrentUser(data);
} catch(err) {
console.log(err)
}
};
useEffect(() => {
getData();
getCurrentUser();
}, [coinData, currentUser]);
if you do so, you will be able to view the exact error and fix it.