Bug in React.js custom fetch hook - javascript

I have the following custom hook, useFetch, in my React (v18.1.0) project to fetch data from a Node.js server.
export default function useFetch(url, requestType, headers, body) {
const [error, setError] = useState(false);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
console.log('Inside useFetch hook');
useEffect(() => {
console.log('Inside useEffect in the useFetch hook');
const controller = new AbortController();
async function retrieveData(reqUrl) {
try {
console.log('Inside the useFetch try block');
const res = await fetchData(
reqUrl,
requestType,
headers,
body,
controller.signal
);
console.log('Done with fetch; this is the server response ', res);
setData(res);
setLoading(false);
console.log('Done with useFetch try block.');
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(true);
setData(err);
setLoading(false);
}
}
}
retrieveData(url);
return () => {
controller.abort();
};
}, []);
return { loading, error, data };
}
My useFetch hook uses a function called fetchData to send a fetch request to the server.
async function fetchData(url, requestType, headers, payload, abortSignal) {
console.log('Inside the fetch function');
let res = await fetch(url, {
method: requestType,
headers: headers,
body: payload,
signal: abortSignal ? abortSignal : null,
});
if (res.ok) {
const resJson = await res.json();
console.log('Returning value from fetch function');
return { status: res.status, response: resJson };
} else {
await fetchErrorHandler(res);
}
}
The useFetch hook is invoked once in my VideoPlayer component.
function VideoPlayer() {
const { videoId } = useParams();
const url = `http://localhost:5000/videos/${videoId}`;
const { loading, error, data } = useFetch(url, 'GET', {}, null);
return (
<div>
{loading && <div />}
{!loading && error && <h2>{data.message}</h2>}
{!loading && !error && (
<video width={600} height={450} controls src={data.response.videoUrl}>
Cannot display video player.
</video>
)}
</div>
);
}
The problem I'm facing is that when the VideoPlayer component is mounted to the DOM and useFetch is called, the execution flow looks like this:
Execution flow of useFetch hook. As is seen in the image, everything seems fine until the line Inside the fetch function is printed in the console. After this, the useEffect hook within the useFetch is called again, for reasons I'm unable to understand (my dependency array is empty and, moreover, there's no state change at this point). Then, it tries to execute the fetch another time, aborts it, and then eventually returns a response, presumably to the original fetch request. At the end of this process, useFetch is called yet again. If anyone can help me shed some light on why the hook is behaving this way, instead of simply executing the fetch request once and returning the response, I will greatly appreciate it.

I assume you use React in StrictMode - this is the default for apps created with create-react-app.
In strict mode, effects can fire twice while in development mode. You can verify that this causes your problem by running a production build. If the problem goes away, it is likely caused by StrictMode.
My suggestion is to not change anything - actually, your code seems to work fine: the first effect execution triggers a fetch, then the second effect execution aborts the initial fetch and fetches for a second time. This is exactly, what the code is expected to do.

You can use axios to deal with API

Related

How do I mock this custom React hook with API call

I am new to React testing and I am trying to write the tests for this custom hook which basically fetches data from an API and returns it.
const useFetch = (url) => {
const [response, setResponseData] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(response => {
if(response.ok === false){
throw Error('could not fetch the data for that resource');
}
return response.json();
})
.then(data => {
setResponseData(data);
setIsLoading(false);
})
.catch((err) => {
console.log(err.message);
})
}, [url]);
return {response, isLoading};
}
export default useFetch;
Since I am new to this I have tried many solutions but nothing is working and I am getting confused too. Can you provide me a simple test for this so that I can get an idea as to how it is done?
here's an example https://testing-library.com/docs/react-testing-library/example-intro/ using msw library to mock server call.
If you mock the server instead of the hook, it will be more resilient to changes.
Moreover, I suggest you to use a library like react-query or read source code of them and reimplement them if you want to learn to avoid fetching pitfalls

Returning a value which is currently in promised state

So this is more of a javascript question than a reactjs question. I am creating a protected route in reactjs. In this, I am fetching the '/checkauth' get request to check the authentication and it correctly returns the response to me. However the problem is that since it is an async function, it takes time to return that value and thus my return statement is executed earlier.
This is the code I am having problem with.
const [auth, setAuth] = useState();
const checkAuthentication = async ()=>{
const res = await fetch('/checkauth', {
method : "GET",
credentials: "include",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
});
const data = await res.json();
return res.status===200 ? (data.isAuthenticated) : (false) ;
}
useEffect(async ()=>{
const data = await checkAuthentication();
setAuth(data)
}, []);
return auth ? <Outlet /> : <Navigate to='/signin' />;
Here the auth is always undefined and thus it always navigates to signing.
Use three states.
const [auth, setAuth] = useState("loading");
...
setAuth(data ? "auth" : "not-auth");
...
if (auth === "loading")
return <Loading />
else if (auth === "not-auth")
return <Navigate to='/signin' />
else
return <Outlet />
You can return a loading spinner while fetching the data, and when the request completes the state will be updated and the <Outlet /> component will be rendered.
By the way: When you are passing an async function to the useEffect hook, it returns a promise and useEffect doesn't expect the callback function to return Promise, rather it expects that the callback returns nothing (undefined) or a function (typically a cleanup function).
Try this:
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch('https://yourapi.com');
// convert the data to json
const json = await response.json();
// set state with the result
setData(json);
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
More info:
React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing

useEffect fetch request is pulling data twice

What I Want:
I'm pulling data from an api, then setting the data to state. I've done this inside a useEffect hook, but when I console.log the data afterwards, it's displaying the data twice, sometimes 4 times. I'm at a loss as to why this is happening.
What I've Tried:
console.log within useEffect to see data from source
disabling react developer tools within chrome.
My Code:
// make api call, assign response to data state
const [apiData, setApiData] = useState();
useEffect(() => {
async function fetchData() {
try {
await fetch('https://restcountries.com/v3.1/all')
.then(response => response.json())
.then(data => setApiData(data));
} catch (e) {
console.error('Error fetching api data', e);
};
};
fetchData();
}, []);
console.log(apiData);
Result of console.log:
As was mentioned in the other comment this is due to effects being "double invoked" in strict-mode.
A common solution and one I believe has been suggested by the React team (although am struggling to find where I read this) is to use useRef.
// make api call, assign response to data state
const [apiData, setApiData] = useState();
const hasFetchedData = useRef(false);
useEffect(() => {
async function fetchData() {
try {
await fetch('https://restcountries.com/v3.1/all')
.then(response => response.json())
.then(data => setApiData(data));
} catch (e) {
console.error('Error fetching api data', e);
};
};
if (hasFetchedData.current === false) {
fetchData();
hasFetchedData.current = true;
}
}, []);
If you are using React version 18+, StrictMode has a behavior change. Before it wasn't running effects twice, now it does to check for bugs. ( Actually, it does mount-remount, which causes initial render effects to fire twice)
https://reactjs.org/blog/2022/03/29/react-v18.html#new-strict-mode-behaviors

Async function in useEffect doesn't execute only when i'm rereshing the page

I'm new in react! and I tried to search for a solution but unfortunately, I didn't have an answer.
I'm trying to get profile data from an async function in useEffect, the first call of the component always doesn't show the data (it shows only the interface without the data ) and also doesn't show "loaded" in the console which I use it for testing, as the code shows. however, when I refresh the page all the data is loaded successfully
here's the code :
const [profile, setProfile] = useState({});
useEffect(() => {
async function fetchData() {
try {
/*some code to get the instance and account used is the function getProfile */
const response = await instance.methods.getProfile(account[0]).call();
setProfile(response.profile);
} catch (error) {
alert(
`Failed`,
);
console.error(error);
}
}
fetchData().then(()=>{ console.log('Loaded!') });
});
what should I do ! if there is no solution !
how can refresh the component one it loaded ? or something like that
first, you need to useEffect dependencylist to only run after page render and every rerender you don't give dependencylist it will execute useEffect everytime when state update, the problem is when you first load page there is no data to show what why it's showing nothing because API need time to get data so you can show loader when API is making a call
const [profile, setProfile] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
setLoading(true)
/*some code to get the instance and account used is the function getProfile */
const response = await instance.methods.getProfile(account[0]).call();
setProfile(response.profile);
} catch (error) {
console.error(error);
} finally {
setLoading(false)
}
}
fetchData();
},[]);
return loading ? <div>Loading....</div> : now show your component here

Use function as react hook?

I wanted to use a function as a react hook to wrap fetch requests to an API.
My current hook:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
const [call, setCall] = useState(undefined);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
function fetchFunction() {
fetch(url, {
...options,
});
}
useEffect(() => {
// Only set function if undefined, to prevent setting unnecessarily
if (call === undefined) {
setCall(fetchFunction);
//setCall(apiFetch);
}
}, [auth]);
return call
}
That way, in a react function, I could do the following...
export default function LayoutDash(props) {
const fetchData = useAPI('/api/groups/mine/'); // should return a function
useEffect(() => {
fetchData(); // call API on mount
}, []);
render(...stuff);
}
But it seems react isn't able to use functions in hooks like that. If I set call to fetchFunction, it returns undefined. If I set it to apiFetch, it executes and returns a promise instead of a function that I can call when I want to in the other component.
I initially went for react hooks because I can't use useGlobal outside react components/hooks. And I would need to have access to the reactn global variable auth to check if the access token is expired.
So what would be the best way to go about this? The end goal is being able to pass (url, options) to a function that will be a wrapper to a fetch request. (It checks if auth.access is expired, and if so, obtains a new access token first, then does the api call, otherwise it just does the API call). If there's another way I should go about this other than react hooks, I'd like to know.
Instead of putting your function into useState, consider using useCallback. Your code would look something like this:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
function fetchFunction() {
fetch(url, {
...options,
});
}
const call = useCallback(fetchFunction, [auth]);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
return call
}
The returned function is recreated whenever auth changes, therefore somewhat mimicking what you tried to do with useEffect

Categories