React useEffect hook does not fire when prop dependency changes - javascript

I am using a useEffect hook to make an API call depending on data from a photos prop being passed in.
const ImageCarousel = ({ src, photos }) => {
const [photoList, setPhotos] = useState([]);
const storageRef = firebase.storage().ref();
console.log("photos prop:", photos);
const getImage = photoId => {
return new Promise((resolve, reject) => {
storageRef
.child("images/" + photoId)
.getDownloadURL()
.then(url => resolve(url))
.catch(error => reject(error));
});
};
useEffect(() => {
console.log("trigger");
Promise.all(
photos.map(async photoId => {
const url = await getImage(photoId);
return url;
})
)
.then(photoUrls => {
setPhotos(photoUrls);
})
.catch(e => console.log(e));
}, [photos]);
return (...)
}
I have passed the photos prop into the dependency array of my useEffect hook, so it should fire when the photos prop changes. I can see that the prop is indeed changing in the console, but my useEffect hook does not fire when the prop change comes through. This is what appears in my console:
Why is the useEffect not firing twice? From my understanding it should fire when the component renders the first time, and fire again every time the photos prop changes.

Try to make sure your photos prop is immutable,
meaning, you send a new array each time there is a change and not mutating the array

This may not be the most correct answer, but I've tried this and it works. Hat tip to https://dev.to/stephane/how-to-make-sure-useeffect-catches-array-changes-fm3
useEffect(() => {
console.log("trigger");
Promise.all(
photos.map(async photoId => {
const url = await getImage(photoId);
return url;
})
)
.then(photoUrls => {
setPhotos(photoUrls);
})
.catch(e => console.log(e));
}, [JSON.stringify(photos])); // stringify the array used as the trigger, and it'll catch the change

useEffect will update if you pass the new data, find the sample code below and check the console.log.
import React, { useState, useEffect } from "react";
const EffectCheck = ({value}) => {
console.log('Component Trigger', value);
useEffect(() => {
console.log('Updated Effect', value);
},[value]);
return (
<div>{value}</div>
)
}
export default function() {
const [checkValue, setCheckValue] = useState(Math.random());
const [dynamicCheckValue, setdyamicCheckValue] = useState(Math.random());
return (
<>
<div><h3>No Update</h3>
<EffectCheck value={checkValue}/>
</div>
<div><h3>New Update</h3>
<EffectCheck value={dynamicCheckValue}/>
</div>
<button onClick={() => setCheckValue('Math.random()')}> No Update</button>
<button onClick={() => setdyamicCheckValue(Math.random())}> New Update</button>
</>
);
}

Related

Load props before using to set state in a useEffect hook

I am trying to pass a user address into this Fetch Function, set the value of a state variable equal to the address, and then use that address to make an api call. But as expected, everything runs at the same time and the api call fails because it does not receive the user address.
I am relatively new to useEffect, the below is how I assume a function like this should be written, but evidently I am missing something. It does not return any errors, just a undefined value in the log statement I have below.
const Fetch = (props) => {
const api_key = process.env.REACT_APP_API_KEY;
const [addr,setAddr] = useState([])
const [data,setData] = useState([])
useEffect(() => {
async function Get(){
setAddr(props.useraddress)
}
Get();
}, []);
async function GetNFT() {
useEffect(() => {
axios
.get(
`https://flow-testnet.g.alchemy.com/v2/${api_key}/getNFTs/?owner=${addr}&offset=0&limit=10`
)
.then(res=> {
setData(res.data.nfts);
})
.catch(err=> {
console.log(err);
})
},[]);
}
GetNFT();
console.log(data);
return (
<div>
<script>{console.log('Fetch'+addr)}</script>
{/*
<>
{data.map((dat,id)=>{
return <div key={id}>
<FetchData NFTData={dat} />
</div>
})}
</>
*/}
</div>
)
}
You need a single useEffect that would depend on useraddress that you can destructure from the props, and make an api call that uses the useraddress. You don't need to store useraddress in the state.
const api_key = process.env.REACT_APP_API_KEY
const createUrl = addr => `https://flow-testnet.g.alchemy.com/v2/${api_key}/getNFTs/?owner=${addr}&offset=0&limit=10`
const Fetch = ({ useraddress }) => {
const [addr,setAddr] = useState([])
const [data,setData] = useState([])
useEffect(() => {
axios.get(createUrlcreateUrl(useraddress))
.then(res=> {
setData(res.data.nfts)
})
.catch(err=> {
console.log(err)
})
}, [useraddress])
console.log(data)
return (
// jsx
)
}
Note that the useEffect would be triggered on component's mount, and whenever useraddress changes. If useraddress might be empty or undefined when the component mounts, add a condition inside that avoids the call:
useEffect(() => {
if(!useraddress) return // skip the api call if the address is empty/undefined/null
axios.get(createUrlcreateUrl(useraddress))
.then(res => {
setData(res.data.nfts)
})
.catch(err => {
console.log(err)
})
}, [useraddress])

Uncaught (in promise) Error: Rendered more hooks than during the previous render

enter image description hereI'm using useEffect hook to use fetch api but it doesn't work. when there is only one api it is working fine but when i use another api to fetch data using the useEffect hook in the createData function it gives error.
I did some research and i think it is because of some issues caused in re rendering of component in react, i tried to search for the fix but couldn't find it so I'm posting it, if there is any question kindly ask me in comments I'll give more details about it.
export default function Unpaid({ transporterId, getFn }) {
const [itemData, setItemData] = useState([]);
const [resData, setResData] = useState([]);
const idUrl =
"https://url...";
useEffect(() => {
let mounted = true;
fetch(idUrl)
.then((data) => data.json())
.then((data) => setResData(data));
return () => (mounted = false);
}, []);
console.log(resData, "response data");
const dispatchId = resData.map((item) => item.id);
console.log(dispatchId, "dispatch id");
function createData(
po,
id
) {
useEffect(() => {
fetch(
"https://url+id"
)
.then((data) => data.json())
.then((data) => setItemData(data));
}, []);
console.log(itemData, "yohohoho");
return {
po,
};
}
function Row(props) {
const { row } = props;
const [open, setOpen] = React.useState(false);
return (
<>
jsx content
</>
);
}
const rows = resData.map((item) =>
createData(
item.purchase_order_details.po_number &&
item.purchase_order_details.po_number.length > 0
? item.purchase_order_details.po_number
: "NA",
item.id
)
);
return (
<>
jsx content
</>
);
}
By calling createData inside resData.map(...), you are calling useEffect in a loop, which violates the rules for hooks. You can call hooks only on the top level.
To fix this, you should move the effect outside of createData to the top level, add the dependency resData to the and loop over resData inside the effect.

why wont jsx render firestore data

I'm trying to GET data from firestore and render it, in the most basic way possible. I can console.log the data, but as soon as I try to render it via JSX, it doesn't. Why is this?
import React from 'react'
import { useState, useEffect } from 'react'
import {db} from '../../public/init-firebase'
export default function Todo() {
const [todo, setTodo] = useState()
const myArray = []
//retrive data from firestore and push to empty array
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
}
useEffect(() => {
getData()
}, [])
return (
<>
<div>
<h1>Data from firestore: </h1>
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
</div>
</>
)
}
First, change myArray to State like this:
const [myArray, setMyArray] = useState([]);
Every change in myArray will re-render the component.
Then, to push items in the State do this:
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
setMyArray(oldArray => [...oldArray, doc.id])
})
})
}
You're just pushing the ID in myArray, so when to show try like this:
{myArray.map((id) => {
console.log('hi')
return <h1 key={id}>{id}</h1>
})}
If you look closely,
{myArray.map((doc) => {
<h1>{doc.id}</h1>
console.log('hi')
})}
those are curly braces {}, not parenthesis (). This means that although doc exists, nothing is happening since you are just declaring <h1>{doc.id}</h1>. In order for it to render, you have to return something in the map function. Try this instead:
{myArray.map((doc) => {
console.log('hi')
return <h1>{doc.id}</h1>
})}
In order to force a "re-render" you will have to use the hooks that you defined
https://reactjs.org/docs/hooks-intro.html
function getData(){
db.collection('Todos')
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id +'pushed to myArray')
myArray.push(doc.id)
})
})
// in case the request doesn't fail, and the array filled up with correct value using correct keys:
// i'd setState here, which will cause a re-render
setTodo(myArray)
}

Multiple useEffect not working as expected

useEffect(() => {
debugger;
}, [filter]);
// eslint-disable-next-line
useEffect(async () => {
if (parseInt(localStorage.getItem("lastFetchTime")) + 8640000 > Date.now()) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
await fetch('https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2')
.then((resp) => resp.json())
.then((data) => {
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
})
}
}, []);
I have these 2 useEffect in my program, the first one, with the listener is not being called even if the filter is changed. But it works if I remove the [] from the 2nd useEffect and the 2nd one runs on loop so I cant use it like that. I saw multiple forums, all of which suggests this should work.
import { useState, useEffect } from "react";
import { render } from "react-dom";
const sleep = (ms: number) => new Promise(
resolve => setTimeout(() => resolve('Resolved'), ms));
function App() {
const [filter, setFilter] = useState({ count: 0 });
const [get, set] = useState(0);
useEffect(() => {
console.log('Here');
}, [filter]);
useEffect(() => {
async function myFunction() {
const res = await sleep(5000)
.then(res => console.log(res));
setFilter({ ...filter, count: filter.count + 1 });
}
myFunction();
}, [get]);
return (
<div>
<p>App {get}</p>
<button onClick={() => set((get: number) => get + 1)}>
Click
</button>
</div>
);
}
render(<App />, document.getElementById("root"));
This minor snippet to be working for me as expected.
useEffect cannot be async. If you want to call an async function in useEffect() you need to do it like this:
EDIT: this is the complete useEffect
useEffect(() => {
async function getData() {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const res = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
);
const data = await res.json();
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}
getData();
}, []);
I tested it and it worked as expected (I console.log() in the other useEffect())
There's nothing wrong with the useEffect. It's a bullet proof. But you make sure the following things:
Is filter updated during the component did mount?
The debugger will show up if you have open developer tool.
Isfilter updated during the component did update?
The debugger won't show up.
To make sure whenfilter is updated, use another effect hook but this time without dependency array.
useEffect(()=>{
console.log(filter) // analyze in the console
})
And if the value is updated during the update then you don't need to use dependency array but check the changes inside the effect hook by using some state for that as filter is coming from the update (props).
import { useState, useEffect, useCallback } from "react";
function App() {
const [isLoading, setIsLoading] = useState(false);
const [filter, setRecipeList] = useState({});
useEffect(() => {
// debugger;
}, [filter]);
// eslint-disable-next-line
const fetchData = useCallback(async () => {
if (
parseInt(localStorage.getItem("lastFetchTime")) + 8640000 >
Date.now()
) {
setRecipeList(JSON.parse(localStorage.getItem("recipeList")));
setIsLoading(false);
} else {
const data = await fetch(
"https://api.spoonacular.com/recipes/random?number=20&apiKey=3c6b5aedfaf34bb899d1751ea2feb1b2"
).then((resp) => resp.json());
setRecipeList(data.recipes);
setIsLoading(false);
localStorage.setItem("recipeList", JSON.stringify(data.recipes));
localStorage.setItem("lastFetchTime", Date.now());
}
}, []);
useEffect(() => {
setIsLoading(true);
fetchData();
}, [fetchData]);
return (
<div>
<span>{isLoading ? "loading" : "loaded!"}</span>
{!isLoading && filter && <div>filter size:{filter.length}</div>}
</div>
);
}
export default App;
I think it will work properly.
Thanks.

React-Native infinite loop

I am trying to get data from my firebase-firestore I an showing a loading state to wait for the data to load however when it does load it keeps returning the firestore data infinite times. Please may someone help me.
This is my code Paper is just a custom component
import Paper from '../Components/Paper'
import firebase from 'firebase'
import { useState } from 'react'
const Home = (props) => {
const renderMealItem = (itemData) =>{
return (
<Paper
title={itemData.item.name}
serves={itemData.item.servings}
time={itemData.item.time}
image={itemData.item.imageUri}
/>
)
}
const [loading, setLoading] = useState(false)
const [all, setAll] = useState([])
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
},[])
const checkReturn = () => {
if(all !== undefined){
setLoading(false)
}
}
const getUser = async() => {
try {
await firebase.firestore()
.collection('Home')
.get()
.then(querySnapshot => {
querySnapshot.docs.forEach(doc => {
setAll(JSON.stringify(doc.data()));
});
});
}catch(err){
console.log(err)
}
}
return(
<View style={styles.flatContainer}>
<FlatList
data={all}
keyExtractor={(item, index) => index.toString()}
renderItem={renderMealItem}/>
</View>
)
}
useEffect without second parameter will get executes on each update.
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
})
so this will set the loading and tries to get the user. and when the data comess from server, it will get runned again.
So you should change it to : useEffect(() => {...}, []) to only get executed on mount phase(start).
Update: you should check for return on every update, not only at start. so change the code to:
useEffect(() => {
setLoading(true)
getUser()
}, [])
useEffect(() => {
checkReturn()
})
Ps: there is another issue with your code as well:
querySnapshot.docs.forEach(doc => {
setAll(JSON.stringify(doc.data()));
});
maybe it should be like :
setAll(querySnapshot.docs.map(doc => JSON.stringify(doc.data())));
Try passing an empty array as an argument to useEffect like so:
useEffect(() => {
setLoading(true)
checkReturn()
getUser()
}, [])

Categories

Resources