Is there any benefit to using more than one useEffect? - reactjs

I recently stumbled over some code where two useEffect hooks were used as opposed to one. Is there any difference between using one big function and multiple? The following code does the job, so I guess I'm just curious.
const MyProjects = () => {
const [data, setData] = useState([])
const [dataLoaded, setDataLoaded] = useState(false)
let clone = useRef(null);
async function fetchData() {
await fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(json => {
setData(json)
clone.current = [...json]
setDataLoaded(true)
})
// .then(setData(data.map((item) => ({...item, backgroundColor: true}))))
.catch((err) => {
console.log(err);
});
}
useEffect(() => {
fetchData()
}, [])
useEffect(() => {
if (!dataLoaded) return
const newArray = clone.current.map(item => (
{...item, backgroundColor: 'orange'}
))
// console.log(clone.current)
setData([...newArray])
console.log(data)
}, [dataLoaded]);

Like all things in programming, the answer is still the same, it honestly depends on the use case.
Case 1: Separating for functionality
The code you have given demonstrates this use case. In your code, the first useEffect is used to fetch the data. This has an empty dependency list and so it will only run once on mounting ( First Render ).
useEffect(() => {
fetchData()
}, [])
While the second useEffect will run but return on the First Render, and only run again each time the dataLoaded variable is mutated
useEffect(() => {
if (!dataLoaded) return
const newArray = clone.current.map(item => (
{...item, backgroundColor: 'orange'}
))
// console.log(clone.current)
setData([...newArray])
console.log(data)
}, [dataLoaded]);
Now it makes sense to keep both functionalities separate because if you kept them together. Then on each data modification, fetchData would've run again. So yes there is a functional benefit in separating useEffects depending on which code you want to run on dependency changes.
Case 2: Separating for code readability and debugging
Usually if you have a useEffect that has too many lines of code. It is better to separate them into different useEffects grouping same purpose code into different useEffects. This will help in code readability and easier debugging afterwards.

Related

Is it ok to update state very second?

I am trying to display the count of items in a cart. I want the count to update every second so if a user adds or deletes from cart it will make that request every second and refresh, updating the state and the number will change. I just tried this method and it works fine, but I'd like to know if it's ok to do or if there is a better way of doing it.
const [update, setUpdate] = useState(0)
const [data, setData] = useState([])
let currentUser = 1
const getData = () => {
axios.get(`http://localhost:4000/api/userCart/${currentUser}`)
.then((res) => {
setData(res.data)
setUpdate(++update)
})
}
useEffect(() => {
getData()
}, [update])
useEffect(() => {
setInterval(() => {
getData()
}, 1000);
},[])
I think thats ok, you need just a way to clear this interval when you destroy the component
const timer = useRef<any>(null);
timer.current = setInterval(() => {
//your interval code
}, time);
useEffect(()=>{
return () => {
clearInterval(timer.current);
}
},[])
your first useEffect I think can be a problem, you made a interval and a effect that runs every get
It's okay when you want to give a real-time like experience. If this will be on production you need to consider how many request will be done and the time it can take to resolve and get the data.
There's a pacakge SWR from Vercel team which you can use https://swr.vercel.app/docs/revalidation , it fetches data, validates it's state and serves a cached state when available. Give it a try
If you want to continue with your own implementation then you need to take into consideration this:
Intervals will keep fetching data don't caring if previous fetch was completed. Solution: Fetch data then run a setTimeout and resolve with a new fetch
Clean up. Save each timeout in a Ref and when a component unmounts clear that timeOut
There's no correct way of doing stuff, give any idea you have a try and if it works the just polish it and avoid any side effects as the mentioned above : )
To consider in your current code
In the code you shared, the getData function is being invoked twice, one from interval which then keeps requestin data, and again when you update the update prop.
A refactor idea can be this:
// Out of component body
const UPDATE_INTERVAL = 1000
// In component body
const [update, setUpdate] = useState(0)
const [data, setData] = useState([])
const timer = useRef(null)
useEffect(() => {
const triggerUpdate = setUpdate((n) => n + 1)
const getData = () => {
return axios.get(`http://localhost:4000/api/userCart/${currentUser}`)
}
getData()
.then((res) => {
setData(res.data)
timer.current = setTimeout(triggerUpdate, UPDATE_INTERVAL)
})
.catch(console.error)
return () => {
clearTimeout(timer.current)
}
}, [update])

Troubles with using hook useState()

Trying to use fetched data into hook useState(fetchedData)
const [car, setCar] = useState({images: []});
useEffect( () => {
fetchOneCar(id)
.then(data => setCar(data))
.finally(() => setLoading(false))
},[id]);
const [images,setImages] = useState(car.images)
console.log(images) // -> [] unpredictably empty
console.log(car.images) // -> [{},{},{}] (fetched data)
How to properly set data into useState() in my case ?
ok look first car is {images:[]}
then images is []
and then car turns into whatever data you fetched in use effect
just because you declare useState after use effect doesn't mean it will run after useEffect.
First all the useStates run and then the effects. that's the law.
so there is no unexpected result.
To fix this in yur use effect do this:
useEffect( () => {
fetchOneCar(id)
.then(data => {
setCar(data);
setImages(data)
})
.finally(() => setLoading(false))
},[id]);
According to your code, I expect that you want to fill the images with the result from data. If it is, then you have to put the setImages(data.images) inside the resolved promise, after the setCar(data).
It should be like this one
const [car, setCar] = useState({images: []});
const [images,setImages] = useState();
useEffect( () => {
fetchOneCar(id)
.then(data => {
setCar(data);
setImages(data.images);
})
.finally(() => setLoading(false))
},[id]);
I put the useState() for images at the top for better reading.

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();
}, [])

How to update state array fetched from API in React Hooks?

I'm fetching data from Studio Ghibli API and I am able to successfully fetch it, set the state array of objects and render it in my presentational component. However, I'm trying to create a function which will add new property "keyword" to every object in my state array. The problem is that when i try to copy the state array to manipulate it in my createKeywords function, the returned copy is empty and I'm unable to manipulate it after it being set.
This is the relevant code:
const baseUrl = 'https://ghibliapi.herokuapp.com/'
const [hasError, setErrors] = useState(false)
const [movies, setMovies] = useState([])
useEffect(() => {
fetch(baseUrl + 'films')
.then((res) => res.json())
.then((res) => {
console.log(res);
setMovies(res)
createKeywords()
})
.catch(err => setErrors(true));
}, [])
const createKeywords = () => {
const moviesWithKeywords = [...movies]
moviesWithKeywords.forEach(function(movie){
movie.keyword = 'castle'
});
setMovies(moviesWithKeywords)
}
If i don't call the createKeywords function everything works fine but obviously copying and setting new movies state creates problem. I tried adding [movies] instead of empty array in useEffect and that works but then useEffect runs indefinitely. Thank you in advance, React isn't my strong suit!
The solution seems might not be very obvious. There are cases where setMovies (in general setting the state) is an async operation, which means that even if you setMovies the movies variable is not being updated quite fast and therefore you are already executing the createKeawords function. This means that within the keywords function the movies variable didn't have the chance to update fast enough. I would recommend to pass the res as a parameter in the createKeywords and use this variable to copy the array to the moviesWithKeywords.
Have a look here under the section State Updates May Be Asynchronous
So do something like that:
const baseUrl = 'https://ghibliapi.herokuapp.com/'
const [hasError, setErrors] = useState(false)
const [movies, setMovies] = useState([])
useEffect(() => {
fetch(baseUrl + 'films')
.then((res) => res.json())
.then((res) => {
console.log(res);
setMovies(res)
createKeywords(res)
})
.catch(err => setErrors(true));
}, [])
const createKeywords = (movies) => {
const moviesWithKeywords = [...movies]
moviesWithKeywords.forEach(function(movie){
movie.keyword = 'castle'
});
setMovies(moviesWithKeywords)
}

React state hook doesn't properly handle async data

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
})
})()
}, [])

Resources