React Hooks Infinite Loop - javascript

So I have a project component that is getting data from my Project context. And I'm trying to select the first object in that array of objects and pass it to a new state.
Project Component
const projects = useContext(ProjectContext) // array of objects from context
const [selected, setSelected] = useState({}) // where i will pass projects[0]
const selectProj = (data) => {
setSelected(data) // INFINITE LOOP ERROR
}
if (projects.length > 0) {
selectProj(projects[0])
}
So I'm kinda lost here on what todo.
Update: Answer I Used
const projects = useContext(ProjectContext) // array of objects
const [selected, setSelected] = useState({})
const selectProj = (data) => {
setSelected(data)
}
useEffect(() => {
if (projects.length > 0) {
selectProj(projects[0])
}
}, [projects])

You're getting into an infinite loop because setSelected causes a re-render, and on the re-render projects.length is greater than 0 again, which causes another re-render, and so on.
One way to avoid this is by calling selectProj only if there's no project already selected:
if (!selected && projects.length > 0) {
selectProj(projects[0]);
}

don't change state on render, you can do it in useEffect or give a initial value
useEffect(()=> {
setSelected(projects[0])
}, [])
or
const [selected, setSelected] = useState(projects[0]);

Related

React: How to reset state in custom hook

I have a react component (AddSlides) which is using useUpload custom hook to upload files, in response the hook returns the download urls. The hook is returning the same value when AddSlides component is re-rendered. To avoid this I am calling reset method in useEffect of AddSlides component, but this is causing infinite loop, which is not surprising.
How do I reset the downloadUrls to empty after using it ? (See below reset method.
React Component addSlides.js
export default function AddSlides() {
const [filesToUpload, setFilesToUpload] = useState([]);
const { downloadUrls, setDownloadUrls, reset } = useUpload(
filesToUpload,
"AddSlides"
);
const [allUrls, setAllUrls] = useState([]);
useEffect(() => {
setAllUrls([...allUrls, ...downloadUrls]);
setFilesToUpload([]); // Resetting the files to empty
//reset(); // Resetting the download urls, but this is causing an infinite loop.
}, [downloadUrls]);
return (<>//some code</>)
}
Custom hook useUpload.js
export default function useUpload(files, componentName = "") {
const [downloadUrls, setDownloadUrls] = useState([]);
const reset = () => {
console.log("Setting download urls to empty");
setDownloadUrls([]); // Resetting the value to empty array.
};
useEffect(() => {
// Some code here
}, [files, componentName]);
return {
downloadUrls,
setDownloadUrls,
reset,
};
}
Execute reset only if array length > 0
useEffect(() => {
setAllUrls([...allUrls, ...downloadUrls]);
setFilesToUpload([]); // Resetting the files to empty
if (downloadUrls.length > 0) {
reset()
}
}, [downloadUrls]);

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]
);

How to handle an unchanging array in useEffect dependency array?

I have a component like this. What I want is for the useEffect function to run anytime myBoolean changes.
I could accomplish this by setting the dependency array to [myBoolean]. But then I get a warning that I'm violating the exhaustive-deps rule, because I reference myArray inside the function. I don't want to violate that rule, so I set the dependency array to [myBoolean, myArray].
But then I get an infinite loop. What's happening is the useEffect is triggered every time myArray changes, which is every time, because it turns out myArray comes from redux and is regenerated on every re-render. And even if the elements of the array are the same as they were before, React compares the array to its previous version using ===, and it's not the same object, so it's not equal.
So what's the right way to do this? How can I run my code only when myBoolean changes, without violating the exhaustive-deps rule?
I have seen this, but I'm still not sure what the solution in this situation is.
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
useEffect(() => {
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]
}
Solution 1
If you always need the 1st item, extract it from the array, and use it as the dependency:
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const item = myArray[0];
useEffect(() => {
if(myBoolean) {
setMyString(item);
}
}, [myBoolean, item]);
}
Solution 2
If you don't want to react to myArray changes, set it as a ref with useRef():
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const arr = useRef(myArray);
useEffect(() => { arr.current = myArray; }, [myArray]);
useEffect(() => {
if(myBoolean) {
setMyString(arr.current);
}
}, [myBoolean]);
}
Note: redux shouldn't generate a new array, every time the state is updated, unless the array or it's items actually change. If a selector generates the array, read about memoized selectors (reselect is a good library for that).
I have an idea about to save previous props. And then we will implement function compare previous props later. Compared value will be used to decide to handle function change in useEffect with no dependency.
It will take up more computation and memory. Just an idea.
Here is my example:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function arrayEquals(a, b) {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index]);
}
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const previousArray = usePrevious(myArray);
const previousBoolean = usePrevious(myBoolean);
const handleEffect = () => {
console.log('child-useEffect-call-with custom compare');
if(myBoolean) {
setMyString(myArray[0]);
}
};
//handle effect in custom solve
useEffect(() => {
//check change here
const isEqual = arrayEquals(myArray, previousArray) && previousBoolean === myBoolean;
if (!isEqual)
handleEffect();
});
useEffect(() => {
console.log('child-useEffect-call with sallow compare');
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]);
return myString;
}
const Any = () => {
const [array, setArray] = useState(['1','2','3']);
console.log('parent-render');
//array is always changed
// useEffect(() => {
// setInterval(() => {
// setArray(['1','2', Math.random()]);
// }, 2000);
// }, []);
//be fine. ref is not changed.
// useEffect(() => {
// setInterval(() => {
// setArray(array);
// }, 2000);
// }, []);
//changed ref but value in array are not changed -> handle this case
useEffect(() => {
setInterval(() => {
setArray(['1','2', '3']);
}, 2000);
}, []);
return <div> <MyComponent myBoolean={true} myArray={array}/> </div>;
}

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.

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