React state hook doesn't properly handle async data - reactjs

I'm trying to set a component's state through an effect hook that handles the backend API. Since this is just a mock, I'd like to use the vanilla react methods and not something like redux-saga.
The problem is that while the fetching part works, the useState hook doesn't update the state.
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(data)
console.log(odds, data) // {}, {...actual data}
})()
}, [])
I've tried to pipe the whole process on top of the fetch like
fetch('odds')
.then(res => res.json())
.then(data => setOdds(data))
.then(() => console.log(odds)) // is still {}
But it doesn't make a single difference.
What am I doing wrong?

Basically if you call setOdds, the value of odds does not change immediately. It is still the last reference available at decleration of the hook.
If you want to access the new value of odds after updating it, you would have to either use the source of the updated value (data) if you want to access the value in the same useEffect hook or create another useEffect hook that triggers only when odds has changed:
useEffect(() => {
console.log(odds);
// Do much more
}, [odds]) // <- Tells the hook to run when the variable `odds` has changed.

If you want to see that state has changed in here, you can use
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(prevData => {
console.log(prevData, data) // {}, {...actual data}
return data
})
})()
}, [])

Related

Refactoring useEffect to only require one database call

At the moment, I have a component which completes some backend calls to decide when to start displaying the UI.
It's structured like this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
The purpose here, is that verifyUser() is supposed to run first, and in the response to verifyUser(), a user id is provided by the backend.
const verifyUser = async () => {
if (!localStorage.getItem('auth')) {
return
}
if (localStorage.getItem('auth')) {
await axios.post("/api/checkAuth", {
token: JSON.parse(localStorage.getItem('auth'))
})
.then((response) => {
return setUserId(response.data.user_id)
})
.catch((err) => {
console.log(err)
localStorage.removeItem('auth')
})
}
}
As a result of this, the fetchData() function is supposed to wait until the verifyUser() function has stopped resolving, so it can use the user id in the database query.
However, at the moment it...
Calls once, without the user id
Then calls again, with the user id (and therefore resolves successfully)
Here's the function for reference:
const fetchData = async () => {
console.log("Fetch data called.")
console.log(userId)
await axios.post("/api/fetch/fetchDetails", {
user_id: userId
})
.then((response) => {
// Sets user details in here...
return response
})
.then(() => {
return setFetching(false)
})
.catch((err) => {
console.log(err)
})
}
What I'm trying to achieve here is to essentially remove any concurrency and just run the functions sequentially. I'm not 100% sure what the best practice here would be, so some feedback would be appreciated!
Your useEffect is missing a dependency array argument:
useEffect(() => {
getData()
})
should be:
useEffect(() => {
getData()
}, [])
Without that argument, useEffect will run once each time your component renders. With that argument, it will only run once, when the component is first mounted (ie. after the first render).
If you needed it to depend on another variable (eg. user.id isn't defined on load, but is later on) you could put that variable in the dependency array, ie.
useEffect(() => {
if (!user.id) return;
getData()
}, [user.id])
This version would run once when the component is mounted, then again if the user.id changes (eg. if it goes from null to an actual number).
In React, the useEffect hook accepts two arguments - the first one is a function (this is the "effect"), and the second one is a dependency array. The simplest useEffect hook looks like this:
useEffect(() => {
}, [])
The above hook has no dependency (because the array is empty), and runs only when the component initially mounts, and then goes silent.
If you don't pass in a dependency array as the second argument, as #machineghost said, the hook will run the "effect" function every time your component re-renders.
Now to your specific problem. You want to run fetchData after verifyUser has resolved its Promise, so you'd add the outcome of verifyUser as a dependency to a separate useEffect hook that calls fetchData. In this case, the outcome is setting userId.
So instead of this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
Do this:
useEffect(() => {
verifyUser();
}, []);
useEffect(() => {
if (userId) { // assuming userId has a false-y value before verifyUser resolved
await fetchData();
}
}, [userId])

Why is `useEffect` not re-running when the dependencies change?

Background:
I was trying to fetch all the links for a set of images, then fetch the real assets for images using the links fetched.
let imageLinks = [];
useEffect(() => {
fetch("http://localhost:3001/video/1?offset=0&count=100")
.then((res) => res.json())
.then((res) => {
imageLinks = res.frames;
});
}, []);
useEffect(() => {
Promise.all(
imageLinks.map((link) =>
fetch("http://localhost:3001/" + link).then((res) => res.json())
)
).then((res) => console.log(res));
// update when the image links changed
}, [imageLinks]);
The last useEffect didn't seem to be working, even when imageLinks has been updated in the previous useEffect. Could anyone please tell me why is that?
In your example, imageLinks is just a variable inside the React component. If the component's prop or state was to change, the variable's value would be reset to [] on each re-render.
To keep React in sync with imageLinks current value, you would need to save it in state. To do this, you need to use useState.
In the example below, when setImageLinks is called it will get stored in React's state, the component will re-render and it will be passed to the effect. The effect will then check if imageLinks has changed and run the effect, if so:
const [imageLinks, setImageLinks] = React.useState([]);
useEffect(() => {
fetch("http://localhost:3001/video/1?offset=0&count=100")
.then((res) => res.json())
.then((res) => {
setImageLinks(res.frames);
});
}, []);
useEffect(() => {
Promise.all(
imageLinks.map((link) =>
fetch("http://localhost:3001/" + link).then((res) => res.json())
)
).then((res) => console.log(res));
// update when the image links changed
}, [imageLinks]);
Further to this, it may simplify your code if you group your fetches together rather than having them as separate effects.

how to use a hook and avoid a loop

I want the fetchTasks() function to be called when I start the component, I know that componentDidMount() is used with classes, but in this way useEffect is used, when I use it, I enter a loop, although it does not send me warnings or errors it sends constant requests to API.
const [tasks, setTasks] = useState([]);
const fetchTasks = (e) => {
fetch('/api/tasks')
.then(res => res.json())
.then(data => {
setTasks(data)
console.log(tasks);
})
.catch(err => console.error(err))
}
useEffect(() => {
fetchTasks();
})
I only want to get the API data once when rendering the component.
Would it be correct to take advantage of this loop to use it as a socket with the API?
Thank you very much, I haven't been in React long.
Use square brackets in useEffect like
useEffect(() => {
fetchTasks();
},[])
If you want to call this again on change of anystate then call like
useEffectt(() => {
fetchTasks();
},[state_variable_name])
To only fetch your data onces inside useEffect you have to provide an empty dependancy array.
useEffect(() => {
fetchTasks();
}, [])

Axios and looped promises

I have problem with loop on axis GET request, and I can't understood why.
const [ state, setState ] = useState<any[]>([]);
ids.forEach((id) => {
getData(id)
.then((smth: Map<string, any>[]) => getNeededData(smth, id));
});
console.log(JSON.stringify(state));
and getData (getNeededData is only choose parameters):
export const getData= async (id: string) => {
const response = await Axios.get(`/rest/${id}`)
.then((res: { data: any; }) => res.data);
return response;
};
I should have 2 response (it's 2 id in variable "ids"), but I have first, second, first, second, first, and this in a loop.
Why it's been working like this?
What I can change for fix this?
By putting that forEach at the top level of your component function, you're running it every time the function is called by React to render its contents, which React does when state changes. The code you've shown doesn't set state, but I'm assuming your real code does.
To do it only when the component first mounts, wrap it in a useEffect callback with an empty dependency array:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
ids.forEach((id) => {
getData(id)
.then(/*...*/);
});
}, []);
If all of the results are going in the state array, you probably want to use map and Promise.all to gether them all up and do a single state change with them, for instance:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
Promise.all(
ids.map((id) => {
return getData(id).then(/*...*/);
})
)
.then(allResults => {
// Use `allResults` to set state; it will be an array in the same order
// that the `id` array was in
})
.catch(error => {
// handle/report error
});
}, []);

how to cancel useEffect subscriptions of firebase

I'm not quite understand how useEffect cleanup function work. Because whatever I do I always get warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Here is my code:
useEffect(() => {
setLoading(true)
// Get position list
const getPositionList = db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
})
return () => getPositionList
}, [])
useEffect expects as a return value an unsubscribe/cleanup function, or a null. Your .get() is NOT a subscription, so there's nothing to clean up
BUT useEffect is NOT asynchronous, and the .get() definitively returns a promise that needs a delay, even with the .then(). Your dependency array is empty, so useEffect is only called once - but I suspect your component unmounted before the .get() ever returned.
Your code will need to be closer to:
useEffect(() => {
setLoading(true)
(async () => {
// Get position list
await db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
});
)();
return null
}, [])
the ( async () => {})() creates an anonymous asynchronous function, then executes it. You do want to try to structure your system such that the enclosing component does not unmount before loading is set to false - we don't see that here, so we have to assume you do that part right.
The important take-aways:
useEffect is NOT asynchronous
.get() IS asynchronous
( async () => {}) creates an anonymous async function, and ( async () => {})() creates a self-executing anonymous asynchronous function
useEffect(() => {
setLoading(true)
// below firebase api return promise. hence no need to unsubscribe.
db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
})
return () => {
// any clean up code, unsubscription goes here. unmounting phase
}
}, [])
According #Dennis Vash answer I figure out, that before setting state I have to check is component mounted or not, so I add variable in useEffect and before set state I check I add if statement:
useEffect(() => {
setLoading(true)
let mounted = false // <- add variable
// Get position list
db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
if(!mounted){ // <- check is it false
setPositionsList(data.list)
setLoading(false)
}
})
return () => {
mounted = true // <- change to true
}
}, [])

Resources