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

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

Related

Async/Await in useEffect(): how to get useState() value?

I have the following code snippet. Why is my limit always 0 in my fetchData? If I were to console.log(limit) outside of this function it has the correct number. Also If I dont use useState but a variable instead let limit = 0; then it works as expected
I also added limit as a dependency in useEffect but it just keeps triggering the function
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const fetchData = async () => {
console.log(limit);
const { data } = await axios.post(endpoint, {
limit: limit,
});
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
};
useEffect(() => {
fetchData();
window.addEventListener(`scroll`, (e) => {
if (bottomOfPage) {
fetchData();
}
});
}, []);
When you pass an empty dependency array [] to useEffect, the effect runs only once on the initial render:
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
useEffect docs
The initial state of limit is 0 as defined in your useState call. Adding limit as a dependency will cause the effect to run every time limit changes.
One way to get around your issue is to wrap the fetchData method in a useCallback while passing the limit variable to the dependency array.
You can then pass the function to the dependency array of useEffect and also return a function from inside of useEffect that removes event listeners with outdated references.
You should also add a loading variable so that the fetchData function doesn't get called multiple times while the user is scrolling to the bottom:
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
console.log(limit);
// Prevent multiple endpoint calls when scrolling near the end with a loading state
if (loading) {
return;
}
setLoading(true);
const { data } = await axios.post(endpoint, { limit });
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
setLoading(false);
}, [limit, loading]);
// Make the initial request on the first render only
useEffect(() => {
fetchData();
}, []);
// Whenever the fetchData function changes update the event listener
useEffect(() => {
const onScroll = (e) => {
if (bottomOfPage) {
fetchData();
}
};
window.addEventListener(`scroll`, onScroll);
// On unmount ask it to remove the listener
return () => window.removeEventListener("scroll", onScroll);
}, [fetchData]);

Maximum depth exceeded while using useEffect

I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);

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

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.

useState not updating an array at all

I'm trying to update the state of an array with React Hooks, using an input received from a child component.
This is the code for the array I'm trying to update (in my App.js file):
const [results, setResults] = useState([]);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
console.log(results);
}
The newArray is updated and logged properly, with all the items that are submitted through the child component. But the state of results is never updated, and it always logs an empty array. It should be noted that other useState hooks in my app are working properly, only the one I'm using for this array isn't working. Does anyone know what could be wrong and how can it be fixed?
If it helps, this is the code that submits the items from the child component (Shorten.js) - these hooks are working perfectly fine:
const [urlInput, setUrlInput] = useState("");
const [error, setError] = useState(false);
const changeHandler = event => {
setUrlInput(event.target.value);
}
const submitHandler = event => {
event.preventDefault();
if (urlInput === "") {
setError(true);
}
else {
setError(false);
axios.post("https://rel.ink/api/links/", {
url: urlInput
})
.then(response => {
const newResult = {
original: urlInput,
shortened: "https://rel.ink/" + response.data.hashid
}
props.submit(newResult);
})
setUrlInput("");
}
}
In your example, you cannot guarantee the results state has been updated at the point of your console.log(results). This is because the React state update as asynchronous and applied in batches under the hood.
If you had your console.log call under const [result, setResults] = useState([]) then it will get called on every render pass, and therefore you should see the updated value logged out once the setState function has applied your new state.
For example:
const [results, setResults] = useState([]);
console.log(results);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
}
should log your new state on the next render pass.
You could also put your console.log in a useEffect which will let you know for sure that React knows your results have changed.
const [results, setResults] = useState([]);
useEffect(() => {
console.log(results);
}, [results);
const submitHandler = newResult => {
const newArray = [...results, newResult];
setResults(newArray);
console.log(newArray);
}
This means your console.log(results) will only be called when results changed, rather then on every render.

Pagination on API requests in Javascript/React?

I have an app that fetches data from a movie API. It returns 20 items from page 1.
How would I go about adding the ability for pagination and allowing user to click a button that increases the page number value and returns the items from that page?
Here's my API call:
export const API_KEY = process.env.REACT_APP_MOVIES_API;
export const baseURL = 'https://api.themoviedb.org/3/movie/';
export const language = '&language=en';
export const region = '&region=gb';
export const currentPage = 1;
export const fetchTopRatedMovies = async () => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${currentPage}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
I'm thinking I need to add 1 to currentPage on request however I'm unsure how to set this up.
The function is called using useEffect in React in a functional component.
const [apiData, setApiData] = useState([]);
const [isLoading, setLoading] = useState(false);
const { results = [] } = apiData;
useEffect(() => {
setLoading(true);
fetchTopRatedMovies().then((data) => setApiData(data));
setLoading(false);
}, []);
You need to make currentPage a param of fetchTopRatedMovies, so that you can pass a dynamic value from your functional component.
You should control the current viewed page in the state of your functional component like this:
const [currentPage, setCurrentPage] = useState(1);
Then you can add a button to the rendering of the functional component that in the onClick handler sets the new value of currentPage and triggers the API call. Approximately like this:
<MyButton onClick={() => {
setCurrentPage(currentPage + 1);
fetchTopRatedMovies(currentPage).then(data => setApiData(data));
}}>
I say approximately because instead of doing immediately the call to fetchTopRatedMovies you could leverage useEffect to re-run the API request on each state / prop change. Or even better, trigger the API request using useEffect only when there's a meaningful state / prop change.
The fetchTopRatedMovies method should be improved like this:
export const fetchTopRatedMovies = async (pageNumber) => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${pageNumber}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
This approach can be extended to all the other params of your API call.
Hope this helps!
Usually it's made by adding currentPage to the state, like
const [currentPage, setCurrentPage ] = useState(1);
So when you want to change it, via click or scroll, use setCurrentPage and in your api call it'll still use currentPage but now it'll reference the one in the state.

Categories

Resources