React state not updated correctly when using useState hook - reactjs

I have this example code. I have a state urls which I create using useState hook. I have initialized urls state to empty array. I have another array arr. For each item in arr, I am pushing that item to urls state. When I render contents of urls, only last item pushed is displayed. It seems while updating urls, only last update is taking effect. What could be wrong?
function Hi() {
const [urls, setUrls] = useState([]);
let arr = ["hi", "hello"];
React.useEffect(() => {
arr.forEach(item => {
let url_list = [...urls];
url_list.push(item);
setUrls(url_list)
})
}, []);
return (
<div>
{urls.map(item => (
<Text>{item}</Text>
))}
</div>
)
}

You're updating the state in each interaction of the array.
The problem here is that setState is asynchronous (read), i.e the update doesn't happen instantly. In other words, when you do let url_list = [...urls], on the second and last iteraction, urls is still [], so that's why you're only getting "hello" into it.
You have 2 main approachs in this case:
1. Update the state after you've mapped the entire array.
React.useEffect(() => {
let url_list = [...urls]
arr.forEach(item => {
url_list.push(item);
})
setUrls(url_list)
}, []);
2. Since setState returns the previous state (read), you can do something like this:
React.useEffect(() => {
arr.forEach(item => {
let url_list = [...urls];
url_list.push(item);
setUrls(prevUrls => ([...prevUrls, ...url_list]))
})
}, []);

You are defining url_list inside forEach. This is reset the values inside url_list on each iterations. Declare url_list outside of forEach and it should be working

Related

usestate can change the state value after axios in useEffect

I expected to get the url with category=business,but the web automatically reset my state to the url that dosent have the category.I dont know the reason behind
let {id}=useParams()
const [newsurl,setNewsurl]=useState(()=>{
const initialstate="https://newsapi.org/v2/top-headlines?country=us&apiKey=c75d8c8ba2f1470bb24817af1ed669ee"
return initialstate;})
//console.log(id);
const [articles, setActicles] = useState([]);
useEffect( ()=>{
if(id === 2)
console.log("condition")
setNewsurl("https://newsapi.org/v2/top-headlines?country=de&category=business&apiKey=c75d8c8ba2f1470bb24817af1ed669ee")},[])
useEffect(() => {
const getArticles = async () => {
const res = await Axios.get(newsurl);
setActicles(res.data.articles);
console.log(res);
};
getArticles();
}, []);
useEffect(() => {
console.log(newsurl)
// Whatever else we want to do after the state ha
s been updated.
}, [newsurl])
//return "https://newsapi.org/v2/top-headlines?country=us&apiKey=c75d8c8ba2f1470bb24817af1ed669ee";}
return (<><Newsnavbar />{articles?.map(({title,description,url,urlToImage,publishedAt,source})=>(
<NewsItem
title={title}
desciption={description}
url={url}
urlToImage={urlToImage}
publishedAt={publishedAt}
source={source.name} />
)) } </>
)
one more things is that when i save the code the page will change to have category but when i refresh it ,it change back to the inital state.Same case when typing the url with no id.May i know how to fix this and the reason behind?
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
You can instead, for example, use a useEffect hook that is dependant on the relevant state in-order to see that the state value actually gets updates as anticipated.
Example:
useEffect(() => {
console.log(newsurl)
// Whatever else we want to do after the state has been updated.
}, [newsurl])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "newsurl" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info about this.
setState is an async operation so in the first render both your useEffetcs run when your url is equal to the default value you pass to the useState hook. in the next render your url is changed but the second useEffect is not running anymore because you passed an empty array as it's dependency so it runs just once.
you can rewrite your code like the snippet below to solve the problem.
const [articles, setActicles] = useState([]);
const Id = props.id;
useEffect(() => {
const getArticles = async () => {
const newsurl =
Id === 2
? "https://newsapi.org/v2/top-headlines?country=de&category=business&apiKey=c75d8c8ba2f1470bb24817af1ed669ee"
: "https://newsapi.org/v2/top-headlines?country=us&apiKey=c75d8c8ba2f1470bb24817af1ed669ee";
const res = await Axios.get(newsurl);
setActicles(res.data.articles);
console.log(res);
};
getArticles();
}, []);

React useEffect non-stop fetching

I'm trying to fetch some data from the backend and display those data in a dropdown menu.
const[allGroups, setAllGroups] = useState([])
useEffect(() => {
console.log("useEffect")
// get all Groups
fetch("http://localhost:8080/group/getAll")
.then(response => response.json())
.then(data => {
let arr = []
for (let i = 0; i < data.length; i++){
arr.push(data[i]["groupName"])
}
setAllGroups(arr)
})
}, [allGroups])
And this is where I generate things
<Dropdown.Menu>
<Dropdown.Item href="#/action-1">Action</Dropdown.Item>
{
allGroups.map((group, i) => {
return (
<Dropdown.Item key={i}>{group}</Dropdown.Item>
)
})
}
</Dropdown.Menu>
I noticed the useEffect hook was running basically all the time (even if the value of allGroups didn't change). I thought useEffect only runs when the specified elements on the page rerender. Is there a way to only trigger useEffect when allGroups change? Thanks!
Is there a way to only trigger useEffect when allGroups change?
That's exactly what your code is doing already. However, allGroups is changing each time the function is run (since you're calling setAllGroups as part of that function).
If you only want the items to be fetched once (after component's first render), remove allGroups from the deps array.
If you want to re-fetch the data in response to some user action, define a separate method that can be explicitly called, and then call it appropriately (e.g. when a button is clicked) in addition to calling it within useEffect with an empty deps array.
Just remove allGroups params from useEffect dependencys
this happened because on each render you fetch your data and when fetch success you set new data to all Groups state that make useEffect rerender
const[allGroups, setAllGroups] = useState([])
useEffect(() => {
getAllGroups()
}, [])
const getAllGroups = () => {
fetch("http://localhost:8080/group/getAll")
.then(response => response.json())
.then(data => {
let arr = []
for (let i = 0; i < data.length; i++){
arr.push(data[i]["groupName"])
}
setAllGroups(arr)
})
}
by using allGroups inside the useEffect the useEffect will be re-render the component as long the allGroups change.
you just need to fetch the APIS in the first render for the component that will lead you to just use useEffect with empty dependency like this []. after getting the response just update the state by using setAllGroups. then implement what you need on allGroups
const[allGroups, setAllGroups] = useState([])
useEffect(() => {
console.log("useEffect")
// get all Groups
fetch("http://localhost:8080/group/getAll")
.then(response => response.json())
.then(data => {
setAllGroups(data) <= just update the state with the response here.
})
}, []) <= you don't need to add **allGroups** here

React custom hook doesn't update state

Here i have a example hooks
const useGPS = () => {
const [gps, setGps] = useState({})
useEffect(() => {
setGps({ a: 1 })
}, [])
return [gps]
}
it's pretty simple, but when i use it inside another Component
const Foo = () => {
const location = useGPS()
useEffect(() => {
console.log(location);
}, [])
return (
<div>1</div>
)
}
console always log empty object for first time. Can someone explain what happened and how can i fix it? Sorry about my bad english. Thanks a lot!
To add to Tushar's answer, if you want to fix the behaviour without leaving useEffect running on every update (which can cause bugs in some more complex examples), you can add location to useEffect's dependencies:
const Foo = () => {
const location = useGPS();
useEffect(() => {
console.log(location);
}, [location]);
return <div>1</div>;
};
That way the effect will run only when a new value for location has been generated. You'll still see an empty object the very first time you call console.log, but it will be immediately updated to the generated value.
The value of GPS is only set after the first useEffect is run (inside the custom hook). It is initially empty and when the useEffect(foo component) runs, that empty value is shown.
The value is set successfully, and you can check this if you remove the [] array from the Foo component's useEffect. [] means that it will only run once after mounting, acting as componentDidMount.
export default function App() {
const location = useGPS()
useEffect(() => {
console.log(location);
});
return (
<div>1</div>
)
}
const [location] = useGPS();
you need to destructor location state array

Updated array needs to be set with react hooks but creates infinite loop

in my code below I am fetching images from Firebase Firestore. The url is returned properly and is pushed to the images state
Now the problem is that identical urls are being added (because the useEffect runs to soon or early?)
So in the second useEffect i update the images array and remove duplicates nice and easy with ...new Set, but the problem is that setImages() is also called there which is not allowed since this creates the infinite loop.
So my question is, can someone tell me where to set the updated array uniq to the images state the right way?
Thanks in advance!
const [evtsFiltered, setEvtsFiltered] = useState([])
const [images, setImages] = useState([])
useEffect(() => {
evtsFiltered?.map((item) => {
storageRef
.child(item)
.getDownloadURL()
.then((url) => {
setImages((images) => [...images, url]) // url returns a plain string such as 'http://image-1.png'
})
})
}, [evtsFiltered])
useEffect(() => {
let uniq = [...new Set(images)] // Duplicates are removed
setImages(uniq) // Infinite loop problem
}, [images])
Improvements needed in your code:
You don't need to use optional-chaining with evtsFiltered because its initial value is an empty array.
map() is not the right method to use if you are just going to iterate over the array. You could use map() method along with Promise.all() to fetch image URLs.
Identical URLs could not be because of useEffect hook. Either you have duplicate URLs in the firebase storage or you are not updating the state correctly.
You wouldn't need the second useEffect hook if you update the images state correctly in the first useEffect hook.
Try updating the state using Promise.all() and map() method as shown below:
useEffect(() => {
const arr = evtsFiltered.map((item) => {
return storageRef.child(item).getDownloadURL();
});
Promise.all(arr)
.then(urls => {
setImages(urls);
})
.catch(err => /* handle error */ );
}, [evtsFiltered]);
Using map() method, create an array of promises that you can then resolve using Promise.all() method. Also, remove the 2nd useEffect hook.
It's because you try to update state on which you listen tov updated. One solution is to create another state which contains unique images.
const [evtsFiltered, setEvtsFiltered] = useState([])
const [images, setImages] = useState([])
const [uniqueImages, setUniqueImages] = useState([])
useEffect(() => {
evtsFiltered?.map((item) => {
storageRef
.child(item)
.getDownloadURL()
.then((url) => {
setImages((images) => [...images, url]) // url returns a plain string such as 'http://image-1.png'
})
})
}, [evtsFiltered])
useEffect(() => {
let uniq = [...new Set(images)] // Duplicates are removed
setUniqueImages(uniq) // Infinite loop problem
}, [images])
Another to set unique images just in first effect.
.then((url) => {
setImages((images) => [ ...new Set([ ...images, url])]) // url returns a plain string such as 'http://image-1.png'
})

Fetching data in useEffect with an array as dependency should only be called on new elements

I have an array, which is given as prop into my component named Child. Now, every time a new item is added to the array a fetch against an API should be made.
This array is held in a component named Parent using the useState hook. Whenever I want to add a new item, I have to recreate the array, since I'm not allowed to mutate it directly.
I tried to simplify my use case in the following code snippet:
const Parent = () => {
const [array, setArray] = useState([]);
///...anywhere
setArray(a => [...a, newItem]);
return <Child array={array} />;
}
const Child = ({ array }) => {
useEffect(() => {
array.forEach(element => {
fetch(...);
});
}, [array]);
return ...;
}
Now, my question is: How can I achieve to fetch new data from my API only for the new element but not for the whole array again?
I hope I described my issue good enough. If anything is unclear or missing, let me know.
How about instead fetching the API data in Parent and just passing the end result to Child? That refactoring would provide some benefits:
Parent owns the items array state and knows when and where a new item is added. That makes an incremental fetch very easy. You also get division of container and presentational components for free.
The fetched API data is related to the items array. You probably want to use them together in some way (save api data as state, combine them, etc.). This constellation would promote derived state, which is a more error prone pattern.
Something like following example could already do what you want - add an item via onClick (or somehow else), fetch its data and pass the whole array down to Child:
const Parent = () => {
const [array, setArray] = useState([]);
return (
<div onClick={addItem}>
<Child array={array} />;
</div>
);
function addItem(e) {
const item = createItemSomehow(...)
fetch(...).then(data => setArray([...array, { item, data }]));
}
};
Update:
If you want to keep your structure and API as is, an alternative would be to memoize the previous arrays prop in your Child with a usePrevious hook and look for item changes.
const Child = ({ array }) => {
const prevArray = usePrevious(array);
useEffect(() => {
if (array !== prevArray && array.length) {
//fetch(...)
console.log(`fetch data for index ${array.length - 1}`);
}
});
return (...);
};
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Codesandbox
You could, for example, keep a list of previously fetched items.
const Child = ({ values }) => {
const [fetched, setFetched] = useState([]);
useEffect(() => {
values.forEach(v => {
if (!fetched.includes(v)) {
setFetched(fetched => [...fetched, v]);
fetch(v);
}
});
}, [values, logged]);
https://codesandbox.io/s/affectionate-colden-sfpff

Resources