Multiple Async Rely On Each Other and Promise.all() Causes Render Loop - javascript

I have an async function that pulls data, in the form of a json, from an API. Each consecutive call is relying on what is previously returned. Sometimes when I try to access values pulled from an API, it received null since that has not fully loaded due to async function.
I tried to mitigate this with another useState() function but then it goes into a rendering loop.
The behavior I am looking for, is while the 3 sets of data are loading from the API, the CircularProgress is spinning. Once they are done loading, the page with information actually renders.
Load function
export function Load(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
fetchUrl();
}, [url]);
// console.log(data)
return [data, loading];
}
main function
import React, { useState } from "react";
import CircularProgress from '#material-ui/core/CircularProgress';
const main (props) => {
const id = props.id
const [loading, setLoading] = useState(true)
const url1 = 'http://path/to/api'+id
const [data1, loading1] = Load(url1);
//const [data1, loading] = Load(url1) This works sometimes
const url2 = 'http://path/to/api2' + data1.val1
const [data2, loading2] = Load(url2)
const url3 = 'http://path/to/api3' + data2.val1
const [data3, loading3] = Load(url3)
const [loading, setLoading] = useState(false)
//if (loading1 === false){setLoading(false)}
Promise.all(data2).then(setLoading(false))
const GetValue = (props) => {
//do something with value
props.value=value
console.log(value)
}
return(<> {loading ? (
<Grid
container
spacing={0}
alignItems="center"
justify="center"
style={{ minHeight: '90vh' }}
>
<CircularProgress size="10vh" />
</Grid>
) : (
//render something
<GetValue props={data3}/>
)}
);
}
export default Main
I get this error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
If I just return loading in const [data1, loading] = DataLoader(url1);, and get rid of useState and Promise.all(), I get this error
Error: GetValue(...): Nothing was returned from render. This usually means a return statement is
missing. Or, to render nothing, return null.
I don't understand how GetValue is getting called because loading is still true.

The part
const [loading, setLoading] = useState(false)
//if (loading1 === false){setLoading(false)}
Promise.all(data2).then(setLoading(false))
doesn't make sense. data2 is not a promise (or an array of promises), and you're not passing a function to then() but are just synchronously calling setLoading every time the component is rerendered, which causes the infinite rendering loop.
What you want is a simple
const loading = loading1 || loading2 || loading3;
Also, you should ensure to skip the effect in your Load hook while the url is still undefined.

Related

Consider adding an error boundary to your tree to customize error handling behavior in Functional Component

I have been working on an admin-Ui project. where I require to display data in a table format and do some pagination.
Firstly I have used use effect to fetch some data from an API on page load.
function App() {
const [data, setData] = useState();
const [filteredData, setFilteredData] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
const getData = async () => {
const res = await axios.get(config.endpoint);
setData(res.data);
setFilteredData(res.data);
};
getData();
}, []);
//Handling Search
const handleSearch = (text) => {
setFilteredData(performSearch(text, data));
};
//paging;
const end = page * config.pageSize;
const start = end - config.pageSize;
const filterData = data.slice(start, end);
//change Page Function
const handlePage = (num) => setPage(num);
return (
<div className="App">
<Search handleSearch={handleSearch} />
<UserTable data={filterData} />
{/* <Pages handlePaginate={handlePage} dataLength={data.length} /> */}
</div>
);
}
export default App;
All imports are done properly.
While running this code the application shows an array that data is undefined.
and produces an error
Consider adding an error boundary to your tree to customize error handling behavior.
After referring to the docs it stated that it works only for class components but I am using react functional components. Any idea to solve this issue?
I understood that the "data" usage in pagination is causing errors because the data is undefined before the API call and setData().
Need Help.
Before the data is fetched from the newtwork, the value of state variable data is undefined, because you declared it so:
const [data, setData] = useState();
This is same as :
const [data, setData] = useState(undefined);
I guess the real problem is here:
const filterData = data.slice(start, end);
.slice does not exist on undefined. You can do two things:
Define (Initialize) data as an empty array:
const [data, setData] = useState([]);
Or, you can use Optional Chaining

making set method of useState hook to wait for the variable update

I have a use case where I am using the useState hook to increment value of the variable. Once the value of the variable is incremented then only I need to call a update function.
const [page, setPage] = useState(1);
const fetchMoreData = () => {
setPage(page+1);
updateNews();
};
So in essence I wanted it to be something like await setPage(page+1);. So that once the page is updated then only I fetch the news from the update URL page.
Due to this currently I am getting
index.js:1 Warning: Encountered two children with the same key, `https://english.jagran.com/trending/did-mars-ever-look-like-earth-heres-what-top-nasa-scientist-has-to-say-10033695`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
at div
at div
at div
at div
at InfiniteScroll (http://localhost:3000/static/js/vendors~main.chunk.js:32922:24)
at News (http://localhost:3000/static/js/main.chunk.js:775:89)
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:34951:29)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:35153:29)
at div
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:34582:30)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:34203:35)
at App
This is my component News.js currently
const News = (props)=>{
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalResults, setTotalResults] = useState(0);
const capitalizeFirstLetter = (string)=> {
return string.charAt(0).toUpperCase() + string.slice(1);
}
const updateNews = async ()=>{
props.setProgress(10);
let goToPage = page;
const url = `https://newsapi.org/v2/top-headlines?country=${props.country}&category=${props.category}&apiKey=${props.apiKey}&page=${goToPage}&pageSize=${props.pageSize}`;
props.setProgress(30);
let data = await fetch(url);
props.setProgress(50);
let parsedData = await data.json();
props.setProgress(70);
if(parsedData)
{
setArticles(articles.concat(parsedData.articles));
setLoading(false);
setPage(page);
setTotalResults(parsedData.totalResults);
}
props.setProgress(100);
}
useEffect(() => {
updateNews();
// eslint-disable-next-line
}, [])
const fetchMoreData = () => {
setPage(page+1);
updateNews();
};
return (
<>
<h3 className="text-center" style={{marginTop:'4%'}}>NewsMonkey - Top {`${capitalizeFirstLetter(props.category)}`} Headlines</h3>
{loading && <Spinner/>}
<InfiniteScroll
dataLength={articles.length}
next={fetchMoreData}
hasMore={articles.length < totalResults}
loader={<Spinner/>}
>
<div className="container">
<div className="row">
{articles.map((element)=>{
return (
<div className="col-md-4" key={element.url}>
<NewsItem title={element && element.title?element.title.slice(0, 45): ""} description={element && element.description?element.description.slice(0, 50):""}
imageUrl={element.urlToImage}
newsUrl ={element.url}
author={element.author}
date={element.publishedAt}
source={element.source.name}/>
</div>
)})}
</div>
</div>
</InfiniteScroll>
</>
)
}
export default News
I tried printing the value of goToPage in the update function and as I could see it was 1 every time.
Is there any way to resolve the error or wait for setPage.
Note : I tried the solution to the question which I was getting as suggestion to this question, but that did not work for me.
If you want to increment page property, you probably should use setPage with callback function, like this:
setPage(page => page + 1);
You can achieve desired effect by using useEffect with page in dependency array:
useEffect(() => {
updateNews();
}, [page])

React native," too many re-renders" when calling api in a loop

So what i want to do, to sum it up, is to call an api key multiple times with different variables added to it and add the objects returned into an array. However when i try to make a for loop, which loops through the different variables for the api-key. I get the error, "Too many re-renders, react limits the number of renders to prevent an infinite loop"
for loop:
const getCoinMarketDataApi = useApi(
coinMarketDataApi.getCoinMarketData,
true
);
const top10 = ["bitcoin", "storm", "cardano", "bogged-finance"];
for (let i = 0; i < top10.length; i++) {
getCoinMarketDataApi.request(top10[i]);
}
The function called:
export default useApi = (apiFunc, addToArray = false) => {
const [data, setData] = useState([]);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [index, setIndex] = useState(0);
//Loading marketdata
const request = async (...args) => {
setLoading(true);
const response = await apiFunc(...args);
setLoading(false);
if (!response.ok) return setError(true);
setError(false);
if (addToArray) {
response.data[0].index = index; //Add the variable index to the array
var newArray = [...data, response.data[0]]; //
setIndex(index + 1);
setData(newArray);
} else {
setData(response.data);
}
};
return { data, error, loading, request };
};
I have no idea how to work around this, any input is appreciated, thanks for reading:)
This might help
Have an integer count of API calls loading. IsLoading: IsLoading + 1 and then show the loading indicator if IsLoading > 1
Name each of your IsLoading differently to show different loading indicators. For example, if you had a call to get students and a call to get teachers, you would have IsLoadingStudents and IsLoadingTeachers and have separate loading indicators for each component in the app
maybe u have too many useState.
try using only one state like this.
const [state,setState] = useState({
data:[],
error:false,
loading:false,
index:0
});
setState({...state,loading:true});
setState({...state,data:[],loading:false});
Don't make any API calls inside a loop, very bad practice, see if API accepts a list of inputs

React higher order function to return hook

Currently, I have a custom fetching data hook written in javascript and it works
import {useState, useEffect} from 'react';
const useApi = apiName => id => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
Then I can use pass in what api I want to call to get the hook. For examples:
const useCustomer = useApi("customer")
const useHello = useApi("hello")
.....
const {response, loading, error} = useCustomer("id_1")
It works fine.
Then, I try to convert to typescript
const useApi = (apiName:string) => (id?:string) => {
const [response, setResponse] = useState({})
.......
}
and eslint complains that
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I would like to know whats wrong with this approach, I know I can have something like:
const useApi = (apiName:string, id?:string) => {}
or disable the eslint(react-hooks/rules-of-hooks)
But just curious about whats the potential problems having higher order function of hook since it actually return the response .
Thanks
When you name you function with prefix hooks, eslint thinks of it as a custom hook according to the general convention. Now that implements useState in a nested function which is why it gives you an error
The best way to write the above code is to not use currying function but pass in the apiName as a param directly
const useApi = (apiName, id) => {
const [response, setResponse] = useState();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetching = async () => {
setLoading(true);
const data = await fetch(`/api/${apiName}${id ? `/${id}` : ""}`)
.then((x) => x.json())
.catch((error) => setError(error));
setResponse(data);
setLoading(false);
};
useEffect(() => {
fetching();
}, [id]);
return { response, loading, error };
};
and use it like
.....
const {response, loading, error} = useApi("customer","id_1");
P.S. Hooks are meant to be an alternative to HOC's and there is no point writing a hook if you use it as an HOC itself
There's a much easier way if you don't need the id variable to be in the hook. The reason why you get the warning is because your hooks are in your CB instead of your root function.
Correct Example:
const useApi = (apiName:string) => {
const [response, setResponse] = useState({});
return (id?: string) => {
.......
};
}

How to set inital state as props in react with hooks?

I want to set state as props in react using hooks and I'm getting error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
▶ 34 stack frames were collapsed.
My code:
First Component :
const List = () => {
const [items, setItems] = useState([{}])
useEffect(() => {
const fetchData = async () => {
const data = await fetch(
'http://localhost:5000/api',
);
const result = await data.json();
setItems(result);
};
fetchData();
}, []);
return (
<ActualList items={items}/>
)
}
and the second component:
const ActualList = props => {
const [items, setItems] = useState([{}])
setItems(props.items)
}
...
You are calling setItem in every render. Each time you change a state value, your component will be re-rendered, which will cause another state change, another re-render....
You should conditionally call setItems
You can directly pass props to useState:
const ActualList = props => {
const [items, setItems] = useState(props.items) // pass props.items as an initial state
}
So I eventually figured out how to do this, in case someone needs it here is the code :
const [items, setItems] = useState([{}]);
useEffect(() => setItems(props.items), [props])

Categories

Resources