Have a custom hook run an API call only once in useEffect - javascript

I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};

The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.

I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.

Related

Why is useFetcher causing an re-render infinite loop?

I have an input. On every change to the input, I want to call an API.
Here's a simplified version of the code:
// updated by input
const [urlText, setUrlText] = useState("");
const fetcher = useFetcher();
useEffect(() => {
if (urlText === "" || !fetcher) return;
fetcher.load(`/api/preview?url=${urlText}`);
}, [urlText]);
The issue is, when I put urlText inside of the dependencies array, there is an infinite rendering loop, and React claims the issue is I might be updating state inside of the useEffect. However, as far as I can tell, I'm not updating any state inside of the hook, so I'm not sure why an infinite re-render is happening.
Any thoughts?
The fuller version of the code is:
Note: The bug still happens without the debounce, or the useMemo, all of that stuff is roughly irrelevant.
export default function () {
const { code, higlightedCode } = useLoaderData<API>();
const [urlText, setUrlText] = useState("");
const url = useMemo(() => getURL(prefixWithHttps(urlText)), [urlText]);
const debouncedUrl = useDebounce(url, 250);
const fetcher = useFetcher();
useEffect(() => {
if (url === null || !fetcher) return;
fetcher.load(`/api/preview?url=${encodeURIComponent(url.toString())}`);
}, [debouncedUrl]);
return (
<input
type="text"
placeholder="Paste URL"
className={clsx(
"w-full rounded-sm bg-gray-800 text-white text-center placeholder:text-white"
//"placeholder:text-left text-left"
)}
value={urlText}
onChange={(e) => setUrlText(e.target.value)}
></input>
);
}
The problem you're having is that fetcher is updated throughout the fetch process. This is causing your effect to re-run, and since you are calling load again, it is repeating the cycle.
You should be checking fetcher.state to see when to fetch.
useEffect(() => {
// check to see if you haven't fetched yet
// and we haven't received the data
if (fetcher.state === 'idle' && !fetcher.data) {
fetcher.load(url)
}
}, [url, fetcher.state, fetcher.data])
https://remix.run/docs/en/v1/api/remix#usefetcher
You might by setting state in useFetcher hook, please check code of load method from useFetcher.
Update: I'm silly. useDebounce returns an array.

React - Updating previous array with new values using useState hook

I created a home page that contains three components HomeHeader, CompanyList and ScrollToTopBtn.
Focusing on CompanyList it is a container component that shows list of CompanyCards that were fetched using an API in Home page. List of companies are initialized in Home page like this
const [companies, setCompanies] = useState([]);
The problem is that initially after making the API call, I use spread operator to update the companies list like this
const fetchedCompanies = await fetchFeedData(page);
console.log(fetchedCompanies);
setCompanies((prevCompanies=>{return [...prevCompanies,fetchedCompanies]}));
console.log(companies);
But an error occurs Uncaught TypeError: prevCompanies is not iterable since I believe that companies list is initially empty.
I tried to use another approach by using concat method but companies list stayed empty and showed no companies found message.
Below is the source code for Home page
const Home = () => {
const [page, setPage] = useState(1); //In order to avoid showing "no companies found" initially
const [isLoading, setIsLoading] = useState(true);
const [companies, setCompanies] = useState([]);
const [isError, setIsError] = useState(false);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setIsError(false);
try {
const fetchedCompanies = await fetchFeedData(page);
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)})); //problem here
setHasMore(fetchedCompanies.length > 0)
} catch (e) {
setIsError(true)
setHasMore(false);
}
setIsLoading(false)
}
fetchData();
}, [page])
return (
<div>
<ScrollToTopBtn />
<Helmet>
<title>{stackinfo.title} - {home.title}</title>
</Helmet>
<HomeHeader />
{
isLoading ? (<LoadingSpinner />) :
isError ? (<ErrorBoundary message={somethingWrongError.message}></ErrorBoundary>) :
<CompanyList companies={companies} ></CompanyList>
}
<Container className={"mt-5"}>
<Row>
<Col sm={12}>
{(!isLoading && !isError && hasMore) &&
<CustomBtn
ButtonText={showMoreBtnText.message}
onClick={() => { console.log("Need to increment number of pages") }}
></CustomBtn>}
</Col>
</Row>
</Container>
</div>
)
}
I tried to check the fetchedCompanies and companies after making the API call
const fetchedCompanies = await fetchFeedData(page);
//Returning new array containing previous companies and recent fetchedCompanies
console.log(fetchedCompanies);
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)}));
console.log(companies);
fetchedCompanies logged an array that has 9 elements, while companies as mentioned above logged empty array [].
Sorry if I missed something, I am still new to React.
You can do this below to update
setCompanies([...companies, ...fetchedCompanies])
If fetched Company are totally new Array containing all with previous record then just do it below;
setCompanies([...fetchedCompanies]);
//OR
setCompanies(fetchedCompanies);
If you have empty strings then do this below
setCompanies([...companies, ...fetchedCompanies.filter(com => !com)]);
This is trying to mutate state, not return a new state (or maybe you just forgot the return keyword?):
setCompanies((prevCompanies=>{prevCompanies.concat(fetchedCompanies)}));
Your previous attempt was closer:
setCompanies(prevCompanies=>{return [...prevCompanies, fetchedCompanies]});
But if fetchedCompanies is also an array as the name implies then you forgot its spread operator:
setCompanies(prevCompanies=>{return [...prevCompanies, ...fetchedCompanies]});
Without that the resulting array would be weird at best.
You can also simplify a little:
setCompanies(prevCompanies=>[...prevCompanies, ...fetchedCompanies]);
And if you don't expect these calls to overlap at all, you could potentially simplify a lot:
setCompanies([...companies, ...fetchedCompanies]);
If after that there is still an empty string then it seems that the data has an empty string. In that case you'd have to filter that out manually, and where you do that is really up to you if the act of filtering might mess with the rest of the logic you have there (the hasMore value, for example). But you can append .filter() to the resulting array any time you like. When fetching, when updating state, or even just when rendering.
You would be better to look where the empty string is coming from and try to resolve that issue but if you simple want to remove the empty string, use the following.
prevCompanies.concat(fetchedCompanies).filter(Boolean)

Firestore database infinite loop React Js

I have my react js app linked to cloud Firestore and I'm trying to display the objects on my js file.
I have only 1 object in my Firestore but it keeps reading in a loop and i cant figure out why.
Code from explore.js (display objects from Firebase)
const [nft,setNft]=useState([])
const getNft= async()=>{
const nft = await fs.collection('NFT').get();
const nftArray=[];
for (var snap of nft.docs){
var data = snap.data()
data.ID = snap.id;
nftArray.push({
...data
})
if(nftArray.length===nft.docs.length){
setNft(nftArray);
}
}
}
useEffect(()=>{
getNft();
})}
{nft.length > 0 && (
<div>
<div className='cardContainer'>
<Nft nft={nft}/>
</div>
</div>
)}
{nft.length < 1 && (
<div className='loading'>Loading products..</div>
)}
Infinite loop console
useEffect has a "trigger" property.
ex:
Will run on every render
useEffect(() => {
//do something
});
Will run only once
useEffect(() => {
//do something
}, []);
Will run when given properties changes
useEffect(() => {
//do something
}, [someProperty, someOtherProperty]);
In your case, you are calling the async function, the async function updates the state, and causes a rerender. Since you don't have any trigger (or empty array) added to the useEffect, it will run again, and again, and again.

why do i get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop

In my React.js code Im fetching from an API and I m getting this error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
PLEASE WHAT AM I DOING WRONG?
Here's my fetch component in App.js
const App = () => {
const [records, setRecords] = useState([])
const [loading, setLoading] = useState({ loading: false })
useEffect(() => {
(async function() {
try {
setLoading(true)
const res = await fetch('http://api.enye.tech/v1/challenge/records?limit=20&skip=0')
const data = await res.json();
setRecords({ records: data})
setLoading(false)
console.log(data);
} catch (err) {
console.error(err);
}
})()
}, [])
if(loading) {
return <Preloader />
}
return (
<Fragment>
<SearchBar />
<div className='container'>
<Records loading={setLoading()} records={setRecords()} />
</div>
</Fragment>
);
}
And this is where I'm passing in the fetched data as props
const Records = ({ records, loading }) => {
return (
<div className={styles.p__container}>
<div className="row">
<div className="col-sm-8">
<div className="py-3">
{records.length} Records
</div>
</div>
</div>
<div className={styles.p__grid}>
{records.map(record => (
<RecordItem key={record.id} record={record} />
))
}
</div>
<div className={styles.p__footer}>
</div>
</div>
)
My integrated terminal shows no error but I get this error in my browser console
Im also trying to see if I can fetch just 20 profiles from the API instead of 100
http://api.enye.tech/v1/challenge/records
It's likely here, when you call setLoading() and setRecords on every render:
<Records loading={setLoading()} records={setRecords()} />
You probably just want to pass a the loading and records variables:
<Records loading={loading} records={records} />
The error is in the line
<Records loading={setLoading()} records={setRecords()} />
By writing setLoading() and setRecords() you are essentially changing the states loading, and records. This ensures a re-render, since states are being changed
I believe, you are trying to pass the current loading status and records array as props to component Records. To do so, you should pass the values like
<Records loading={loading} records={records} />
That should clear out the too many re-renders error
Suggestion on useState usage
You are initializing the states loading and setRecords in a different way than the way you are using it later.
For eg., you initialize loading as {loading: false} but later call setLoading(false). You should initialize the loading state as a boolean state using
const [loading, setLoading] = useState(false);
// example usage
setLoading(true);
Similarly, for records, you are initializing with an empty array useState([]), but later setting records state as an object using setRecords({ records: data})
You should use either one of the approaches
/** Approach 1 (Array typed) */
const [records, setRecords] = useState([]);
// example usage
setRecords(data);
/** Approach 2 (Object typed) */
const [records, setRecords] = useState({records: []});
// example usage
setRecords({records: data});
React has a property that whenever the value of states in a React component get updated, the component is re-rendered. Here you are passing the setLoading() and setRecords() as props to the Records component. You should pass records and loading instead. Every time setLoading() and setRecords() get updated, your component is re rendered and as a result an infinite loop of re rendering takes place.

React js - unable to set state of variable properly

I have a functional component in React and Im having some troubles setting the state of a variable correctly. I have a Pagination component which I am using for my website and I want to know the page I am on when I click page 2 or 3 etc. I decided to set the state of the page with the handlepage function if u see below which sets the page according to which page I press. However in my other function which is onChangePage, I cannot get the correct page value when I console log which I thought should work since Ive already set the state of the page with the onchange in the pagination component. Maybe this is happening because in the pagination component I am calling both functions at the same time that causes the problem. Please tell me how to solve this, it would really help alot I need the correct value of the page in onChangePage.
function Build() {
const [page, setPage] = React.useState(1);
const handlePage = (event, value) => {
setPage(value);
}
// Function for pagination, calls redux
const onChangePage = (name, val, page) => {
console.log(page)
console.log("PAGING HERE")
dispatch(getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val]));
}
return(
<Pagination
count={5}
onClick={() => onChangePage("Price (Lowest to Highest)", "selling_Price ASC", page)}
page={page}
onChange={handlePage} />
);
}
export Build;
React state updates are asynchronous and processed between render cycles. You are likely executing both callbacks simultaneously, so the enqueued state update from one isn't going to be available in the other.
Is there a way to pass these 2 strings into handlePage?
Yes, you can proxy the onChange event object and value to your handler. Think something along the lines of an anonymous function as follows:
const handlePage = (event, page, string1, string2) => {....}
...
onChange={(event, page) => handlePage(event, page, string1, string2)}
Below I've written handlePage as a curried function to first consume the two strings you want passed and returning a callback function to consume the onChange event object and new page value.
const handlePage = (name, val) => (event, page) => {
console.log(page);
console.log("PAGING HERE");
setPage(page);
dispatch(
getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val])
);
};
Code
function Build() {
const [page, setPage] = React.useState(1);
const handlePage = (name, val) => (event, page) => {
console.log(page);
console.log("PAGING HERE");
setPage(page);
dispatch(
getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val])
);
};
return (
<Pagination
count={5}
page={page}
onChange={handlePage("Price (Lowest to Highest)", "selling_Price ASC")}
/>
);
}

Categories