React Firebase read data results in too many renders - reactjs

I'm using Firebase realtime database for my react project. I try to follow the firebase documentation and use "onValue()" to retrieve data. Here is my code:
export default function Home() {
const {currentUser} = useAuth();
const [userinfo,setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
setUserinfo(data);
})
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
This would result in an error of too many re-renders. I don't know how to retrieve the data. If I use
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
console.log(data);
})
then the proper data would print out in the console perfectly fine. I also tried the following:
let info;
onValue(ref(db,`users/${uid}`),snapshot=>{
const data = snapshot.val();
info = data;
})
console.log(info)
but info would just be undefined. I can't seem to figure out the problem here. How can I use the data?

It throws error too many re-renders because you are not using any lifecycle hook or function to update/change state value and once you update your state it will again re-render your whole component and then again you update the state and the same thing happens in the loop causing too many re-renders.
So to avoid this you need to put code that is responsible for listening to changes from DB and changing state inside a block which will only get called on specific events or function calls or etc.
In your case, I suggest using useEffect hook. see below code -
export default function Home() {
const { currentUser } = useAuth();
const [userinfo, setUserinfo] = React.useState();
const uid = currentUser.uid
const db = getDatabase();
// this useEffect will get called only
// when component gets mounted first time
useEffect(() => {
// here onValue will get initialized once
// and on db changes its callback will get invoked
// resulting in changing your state value
onValue(ref(db, `users/${uid}`), snapshot => {
const data = snapshot.val();
setUserinfo(data);
})
return () => {
// this is cleanup function, will call just on component will unmount
// you can clear your events listeners or any async calls here
}
}, [])
console.log(userinfo);
return (
<main id="home">
<Hero />
</main>
)
}
Note - I have not worked with firebase real-time DB recently but by looking at the code and error I have added this answer, let me know if anything needs correction.

Related

react-router-dom state returns error on page refresh

I am new to react-router-dom, I was calling Data inside
of the ParentPage.jsx and mapped it using Card.jsx and it returned cards of data. In the
Card.jsx I passed the data to the ChildPage page using <Link/> and it worked, but if I'm going to refresh the child page it returns an error TypeError: Cannot read properties of undefined. I have also tried storing the data on the localStorage but it is still returning the same error. I hope someone can help me.
Here are my code snippets.
ParentPage.jsx
const [establishment, setEstablishment] = useState([]);
const Data = () => {
...
};
const cards = useMemo(() => {
return establishment.map((establishment) => (
<Card establishment={establishment} />
));
});
...
{cards}
...
Card.jsx
const [details] = useState(establishment);
return (
<>
<text>{details.name}</text>
<Link
to={{
pathname: "/establishments/details",
state: { details },
}}
>
<Button>
Details
</Button>
</Link>
</>
);
ChildPage.jsx
const {state} = useLocation();
const [data, setData] = useState(state?.details);
useEffect(() => {
window.localStorage.setItem(data.id, JSON.stringify(data));
}, [data]);
useEffect(() => {
const updatedData = window.localStorage.getItem(data.id);
if (updatedData !== null) setData(JSON.parse(updatedData));
}, []);
...
...data.color
...
Here is the Error
Issue
You are on the right track, but the logic is a little mixed up. Route state is very transient, it only exists during the route transition and while the receiving component remains mounted. Reloading the page reloads the entire React app. Any state in memory is lost.
Current code:
const location = useLocation();
const [data, setData] = useState(location.state?.details); // (A)
useEffect(() => {
window.localStorage.setItem(data.id, JSON.stringify(data)); // (B)
}, [data]);
useEffect(() => {
const updatedData = window.localStorage.getItem(data.id);
if (updatedData !== null) setData(JSON.parse(updatedData));
}, []);
...
.... data.color // (C)
...
Here's what I see occurring:
Navigate to child page with defined state, the data state is initialized to location.state.details (A), and the component renders with defined data.color (C). No error.
The first useEffect hook runs and persists the data state to local storage under the defined key data.id (B).
The second useEffect hook runs and reads from localStorage using defined data.id key and since it's not null enqueues a data state update.
Reload the page.
The app remounts. This ChildPage component remounts. The data state is initialized to the undefined location.state value (A). Error thrown accessing data.color on the initial render (C).
Solution
The data state should be initialized from location.state.data if it exists, then fallback to localStorage if it exists, then possibly fallback to a base value. Use only a single useEffect hook to persist the local state to localStorage when it updates. Use a storage key that is always defined.
const { state } = useLocation();
const [data, setData] = useState(() => {
return state?.details || JSON.parse(window.localStorage.getItem("details")) || {};
});
useEffect(() => {
window.localStorage.setItem("details", JSON.stringify(data));
}, [data]);
...
.... data.color
...
I think the problem is with the parsing of data not the link it self. It would have helped a lot if you had showed some code and the full error message
For this kind of usage, the best practice would be using query param in react-router-dom, so that you can pass your value and by refreshing the page will work the same, you can check if there is no query param in the child component you can redirect the user back
I think this blog will help to handle it https://denislistiadi.medium.com/react-router-v6-fundamental-url-parameter-query-strings-customizing-link-57b75f7d63dd

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

Trying to pass an array to another component that I'm receiving through an async function

Essentially I have a page where I map out a list of files and after the user is down I call them from an api. I then package the file with other meta data. However my issue is that when I pass back my package to my frontend I am getting an error about passing back a promise. I'm using await on async function I know this is causing a promise to be passed to my variable but I am unsure how else to pass back my file with its meta data. Any suggestions for different approaches or a more proper way to use async functions in order to achieve my goal?
Code:
const gatherFileNames = async() =>{
let fams = defineFams()
let finishedCells = []
finishedCells = await QueryFiles(fams)
return finishedCells
}
export default async function GenerateGraphPage(){
let allCells = await gatherFileNames()
return(
<div>
<DefineHeader />
<CellGraph cells = {allCells}/>
</div>
);
}
Notes:
finishedCells is holding an array of objects
Completed Code for reference:
export default function GenerateGraphPage(){
const [data, setData] = useState();
useEffect(()=>{
const getData = async() => {
const fileData = await gatherFileNames()
setData(fileData)
console.log(fileData)
}
getData()
},[])
return(
<div>
<DefineHeader />
<CellGraph cells={data}/>
</div>
);
}
Assuming QueryFiles returns a promsie this might be what you are looking for, I also refactored your code a little bit, I hope you don't mind.
const gatherFileNames = () =>{
return QueryFiles(defineFams())
//no async
}
If someone knows how the async works in this example feel free to copy this answer to explain it. Because I don't know the technical terms but this worked for me.
export default async function GenerateGraphPage(){
let allCells = await gatherFileNames()
//async
return(
<div>
<DefineHeader />
<CellGraph cells={allCells}/>
</div>
);
}
I would also recommend using useState and a useEffect hook to call gatherFileNames so it isn't called on every render which could slow your website down.
export default async function GenerateGraphPage(){
const [data, setData] = useState();
useEffect(() => {
let FileData = [];
const getData = async () => {
//to use await in a useEffect you will need to make an async function
// and call it first in the useEffect
fileData = await gatherFileNames();
}
getData();
// getData called here before handling any other synchronous logic
setData(fileData)
}, []) //this empty array means this useEffect will run once when the page loads
//if you put 'dependencies here such as [data,setData]'
//this will run everytime data and setData are used
return(
<div>
<DefineHeader />
<CellGraph cells={data}/>
// using data here
</div>
);
}

How to make setting state with useEffect() to run on page refresh?

My code is not long or complicated at all. It's simple. so please read!
(Im using react + next.js)
In the root file, app.js, I have useEffect to fetch photo data. This data array will be used in a page component so I pass it down from app.js via <Component.../>
function MyApp({ Component, pageProps }) {
const [photoData, setPhotoData] = useState([]);
const [user, setUser] = useState([]);
useEffect(() => {
const getPhotos = async () => {
try {
const photoData = await axios.get(
"https://jsonplaceholder.typicode.com/albums"
);
setPhotoData(photoData.data);
} catch (error) {
console.log(error);
}
};
getPhotos();
}, []);
useEffect(() => {
//code for finding user. no external api used.
setUser(user);
}
}
}, []);
const passedProps = {
...pageProps,
photoData,
user
};
return (
...
<Component {...passedProps} />
)
Then I pass the data (photodata) from a Home component to a (app.js 's) grandchild component, an Photo component
export default function Home({ photoData, user }) {
return(
<Photo photoData={photoData} user={user} />
)
In Photo component, I am receiving photoData and trying to set a state for photoArr with the default state of photoData.
When the entire app is first loaded, the photoData is passed down to the Photo component successfully that it sets the state without any issue.
But the main problem is that when I am in the Photo page (photos are loaded) and refresh the page, then it does not set the state for photoArr with photoData. Even though I can console log photoData received from app.js, it does not set state, photoArr, with the default state, photoData.
export default function Photo({ photoData, user }) {
const [photoArr, setPhotoArr] = useState(photoData);
//I have this as state because I change this array
//later on in this component (doing CRUD on the photo array).
console.log(photoData); // this returns the array received from app.js
console.log(photoArr); // []. returns an empty array
console.log(user); // returns the user object received from app.js.
return (
<div>
{photoArr.length > 0 ?
.... code mapping photoArr
: "Data is empty" //It returns this comment when I refresh the page
}
</div>
)
As you can see above, when I refresh the page, I get "Data is empty" meaning photoArr was not set even with the given default state. If I keep refreshing the page multiple times, it still shows a blank page.
From my research, it's due to setting state being asynchronous? So then how can I fix this problem?
Try this:
(In your Photo page)
const [photoArr, setPhotoArr] = useState(null);
useEffect(() => {
if(photoData.length) setPhotoArr(photoData) // If not empty, set the Arr
},[photoData]} // We listen to photoData's change
On page load, there aren't any data in your photoData, and as it pass down to Photo component, react remembers that state.
But with useEffect listen to photoData's change, we can setPhotoArr once the getPhotos function got the data back.

Not awaiting for data in useEffect

I have a useEffect in my component that is waiting for data from the context so that it can set it in state. But its not waiting for the state and is moving on to the next line of code to set isLoading to false.
I'd like it to wait for the data so that I can render the loading.
I tried setting the isFetchingData in the context but I had run into problems where if another component calls it first it would set the isFetchingData state to false.
First call to ReactContext is setting the isLoading sate to false
It is fine for results to come back with no records. The component would render 'No records found'. Therefore, I cannot check the length on state to say if length is zero then keep loading.
Following is my code:
Context
const [activeEmployees, setActiveEmployees] = useState([]);
const [terminatedEmployees, setTerminatedEmployees] = useState([]);
useEffect(() => {
getEmployees()
.then(response => {
/// some code...
setActiveEmployees(response.activeEmployees)
setTerminatedEmployees(response.terminatedEmployees)
});
});
Component
const EmployeesTab = () => {
const { activeEmployees, terminatedEmployees } = useContext(BlipContext);
//Component states
const [isFetchingData, setIsFetchingData] = useState(true);
const [newEmployees, setNewEmployees] = useState([]);
const [oldEmployees, setOldEmployees] = useState([]);
useEffect(() => {
async function getData() {
await setNewEmployees(activeEmployees);
await setOldEmployees(terminatedEmployees);
setIsFetchingData(false);
}
getData();
}, [activeEmployees, terminatedEmployees, isFetchingData]);
if(isFetchingData) {
return <p>'Loading'</p>;
}
return (
// if data is loaded render this
);
};
export default EmployeesTab;
Since you have useState inside your useContext, I don't see the point of storing yet again the activeEmployees in another state.
If you want to have a local loading variable it could something like:
const loading = !(activeEmployees.length && terminatedEmployees.length);
This loading will update whenever getEmployees changes.
And to answer you question, the reason await is not having an effect is because setState is synchronous.

Resources