How do I mock this custom React hook with API call - javascript

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

Related

Bug in React.js custom fetch hook

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

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

React-Native onSubmitEditting taking too much time to execute function

I am following a react-native tutorial but am having some trouble with React contexts and ActivityIndicator, I dont know where the problem lies, but I will try to be as descriptive as possible.
The problem:-
The code :
I am using contexts to provide the app with the location that has been searched and then searching for that location within my mock data, later returning the restaurants around that location.
complete source code at https://github.com/diivi/KiloBite/blob/main/src/services/location/location.context.js
Here I am using the onSearch function and passing it as a context prop to my search box to use with onSubmitEditing.
In your code you call onSearch and there you setILoading(true)
and setKeyword(searchKeyword).
Then in the useEffect you use the keyword you set in onSearch. Your useEffect runs only in keyword changes (see dependencies).
Try to add onSearch in your dependencies (look below).
Or maybe even locationRequest, locationTransform.
I would also try setIsLoading and generally try to put as dependencies everything you use in your useEffect.
const onSearch = (searchKeyword) => {
setIsLoading(true);
setKeyword(searchKeyword);
};
useEffect(() => {
if (!keyword.length) {
return;
}
locationRequest(keyword.toLowerCase())
.then(locationTransform)
.then((result) => {
setIsLoading(false);
setLocation(result);
})
.catch((err) => {
setIsLoading(false);
setError(err);
});
}, [keyword, onSearch]);
BUT, at the end I wonder, why you use a useEffect?
Why don't you just move all the code in onSearch:
const onSearch = (searchKeyword) => {
setIsLoading(true);
// setKeyword(searchKeyword); // not needed
if (!searchKeyword.length) {
return;
}
locationRequest(searchKeyword.toLowerCase())
.then(locationTransform)
.then((result) => {
setIsLoading(false);
setLocation(result);
})
.catch((err) => {
setIsLoading(false);
setError(err);
});
};

Function inside component not receiving latest version of Redux-state to quit polling

I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.

Reactjs Asynchronously load events from calendar - Google Calendar API

I am trying to load events from google calendar api which I fetch with gapi.client.request, the problem is that I can't figure a way how to use async/await properly. My events load after my presentational components. I've used async await before and it worked properly with fetch and other APIs. Is there some other way to wait for google.thenable object. Since it's promise like I thought that it would be easier to handle like I handled promises with fetch before. I'm utterly lost here, any help would be appreciated.
const [events, setEvents] = useState([]);
useEffect(() => {
getEvents();
});
async function get(){
await getEvents();
}
function getEvents(){
init()
.then(() => {
return gapi.client.request({
'path': `https://www.googleapis.com/calendar/v3/calendars/${CALENDAR_ID}/events`,
})
})
.then((res) => {
const allEvents = res.result.items;
setEvents(sortedEvents);
}, (reason) => {
console.log(reason);
});
}
Events don't load before components so they aren't being waited properly. I would like my events to be load asynchronously so that they show simultaneously with other presentational components.
It seems you have some small problems in your code.
First of all, don't forget to make your getEvents() function asynchronous.
Secondly, remember to add a second parameters on your useEffect() method to stop the function from triggering on every single update.
So, you code should look like this:
const [events, setEvents] = useState([]);
useEffect(() => {
getEvents();
}, []);
async function get(){
await getEvents();
}
async function getEvents(){
init()
.then(() => {
return gapi.client.request({
'path': `https://www.googleapis.com/calendar/v3/calendars/${CALENDAR_ID}/events`,
})
})
.then((res) => {
const allEvents = res.result.items;
setEvents(sortedEvents);
}, (reason) => {
console.log(reason);
});
}
You might want to read some more on how to handle APIs with React, here is a good resource for you.
Hope this helps.

Categories