Looping set state react + firestore - reactjs

I'm trying to create a user profile page for my app, for this page I'm trying to get a doc with some user information from firestore, and populate fields that the user can click on and to change. When I added the setTags function to the db call below, it resulted in an infinite loop. Can anyone explain why this happens and how to fix it? I'm very new to using react hooks.
const UserProfile = ({history}) => {
const description = useRef('');
const name = useRef('');
const {currentUser} = useContext(AuthContext);
let [tags, setTags] = useState('');
let uData;
db.collection('Users').doc(currentUser.uid).get().then(doc => {
console.log("checking db")
console.log(doc.data())
uData = doc.data();
description.current = uData.description;
name.current = uData.name
setTags(uData.tags)
})
}

Functional components are run on every render. That means that on every rerender, you open the database to search for the current user, then set the tags. Crucially, updating the state always causes a rerender! This is a core feature of React that allows implicit updating and is one of the main reasons to use React in the first place.
If you want something to be run only once, you can still do that, but you need to simulate componentDidMount() from a class component. So, you can use useEffect:
// Add this import in
import { useEffect } from 'react';
const UserProfile = ({history}) => {
const description = useRef('');
const name = useRef('');
const {currentUser} = useContext(AuthContext);
let [tags, setTags] = useState('');
let uData;
useEffect(() => {
db.collection('Users').doc(currentUser.uid).get().then(doc => {
console.log("checking db")
console.log(doc.data())
uData = doc.data();
description.current = uData.description;
name.current = uData.name
setTags(uData.tags)
})
}, []); // Empty dependency list means this will only run on first render
}

Related

How can I make separate components to loading in order?

For example, I write this in the sandbox for demonstration only, I don't quite remember the structure of my old project, but I remember that I tried something like this, this is written in 1 component, but I think it's not very good practice, because later on, I remember that I split my components into many more for easier management, but at that point, I can't make it load in order, so whenever the Home components are called, it just fetches everything instead of in order, which makes my page load very long due to it try to fetch everything from every component that exists in the Home component.
import {React, useState, useEffect} from "react"
import axios from "axios"
function Home() {
const [loadingSlider, setLoadingSlider] = useState(true)
const [loadingCategory, setLoadingCategory] = useState(true)
const [loadingStuff, setLoadingStuff] = useState(true)
const [sliderData, setSliderData] = useState()
const [categoryData, setCategoryData] = useState()
const [stuffData, setStuffData] = useState()
useEffect(() => {
const fetchSlider = async () => {
const response = await axios.get("sliderUrl")
setSliderData(response.data)
setLoadingSlider(false)
}
const fetchCategory = async () => {
const response = await axios.get("categoryUrl")
setCategoryData(response.data)
setLoadingCategory(false)
}
const fetchStuff = async () => {
const response = await axios.get("stuffUrl")
setStuffData(response.data)
setLoadingStuff(false)
}
fetchSlider()
fetchCategory()
fetchStuff()
} , [])
return (
<>
{
loadingSlider ? "Loading slider" : {sliderData}
}
{
loadingCategory ? "Loading category" : {categoryData}
}
{
loadingStuff ? "Loading stuff" : {stuffData}
}
</>
)
}
So with this practice, if I split 3 sliderData, categoryData, stuffData into 3 different components, and I just want to call it in Home component for easier management, how can I let it load in order?
Each component will fetch inside of it separately.
For example:
function Home() {
return (
<>
<SliderComponent/>
<CategoryComponent/>
<StuffComponent/>
{/* <More components might be added in the future/> */}
</>
)
}
What do I need to do so it can load in order, and not load all of the components at the same time? I want it done fetching the slider first, then it'll proceed to fetch the category, and then so on.
You could do like this:
useEffect(() => {
fetchSlider();
} , [])
const fetchSlider = async () => {
const response = await axios.get("sliderUrl")
if(response.status === 200){
// Call another method you want to load
setSliderData(response.data)
setLoadingSlider(false)
fetchCategory(); // Once Slide data is set call category method
}
}
const fetchCategory = async () => {
const response = await axios.get("categoryUrl")
if(response.status === 200){
// Call another method you want to load
setCategoryData(response.data)
setLoadingCategory(false)
fetchStuff(); // Once Category data is set call stuff method
}
}
const fetchStuff = async () => {
const response = await axios.get("stuffUrl")
setStuffData(response.data)
setLoadingStuff(false)
}
In Short Once you fetch data from method with status 200 then only call another method. And Its your option also even if one method might failed whether you cant to call next method or you want to show error. Hope It might help with your problem.

Make a second API call and return data

I need some help figuring out an issue.
I'm working on a web app and basically I make an API call which is an array of data to get someone's details and return them on the page, but for one param (editedBy) I get the person' id. So, I was asked to make another API call from where I can get that person's name.
import React from 'react';
import {getData} from '.api/calls';
import { connect } from "react-redux";
import {getEditedBy} from './api/calls';
const PersonDetails = ({userId}) => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [editedBy, setEditedBy] = useState('')
const setDetails = user => {
setFirstName(user.firstName);
setLastName(user.lastName);
}
useEffect(() => {
getData(userId).then(res => {
setDetails(res.data)
});
getEditedBy(userId).then(res => {
setEditedBy(res.fullName)
});
}, [userId])
return (
<div>
<p>First Name: {firstName}</p>
<p>LastName: {lastName}</p>
<p>Edited By: {editedBy}</p>
</div>
)
}
export default connect(
state => ({
userId: state.user.userId
})
)(PersonDetails);
How can I return the person's full name and keep that value in place, instead of the id? Any help is appreciated.
This should cover your case:
useEffect(() => {
async function getUserDetailsAsync() {
const userData = (await getData(userId)).data;
const editedBy = (await getEditedBy(userData.editedBy)).data;
setDetails(userData);
setEditedBy(editedBy.fullName);
}
getUserDetailsAsync();
}, [userId]);
If you're not a fan of the await syntax for some reason you can go with the .then chaining but in any case you should make the second API call dependent on the first one.
useEffect(() => {
getData(userId).then(res => {
const userData = res.data;
getEditedBy(userData.editedBy).then(res => {
const editedBy = res.data;
setDetails(userData);
setEditedBy(editedBy.fullName);
});
});
}, [userId]);
In your current solution, you're firing 2 requests simultaneously which will cause completely nondeterministic result for you. First of all, you're querying for the fullName not of the person who edited the post, but for the same person you just queried(if I understood your context correctly). Second, your state will be updated at different times due to the independent requests. Third - one of the requests may fail while the other succeed, thus leaving your component with inconsistent state. Furthermore, you might have problems with trying to update an already unmounted component and you should have this in mind.

How to update react useState without delay?

When I call fetchProducts I want productPage state to be updated before it runs the next line of code since the next line requires the updated state.
The state only gets updated after the function has finished running. No matter where in the fetchProducts function I put console.log(productPage) it returns the state as it was before the function was called.
const [productPage, setProductPage] = useState(1)
const [displayProducts, setDisplayProducts] = useState([])
const fetchProducts = async () => {
setProductPage(productPage + 1) // DOES NOT UPDATE UNTIL AFTER FUNCTION RUNS
const newProductsData = await fetch(`/api/getproducts?page=${productPage}`)
const newProductsArr = await newProductsData.json()
setDisplayProducts(displayProducts.concat(newProductsArr))
}
Is this possible? I've found workarounds to the problem but it feels hacky and not ideal.
I'm using next.js also, I'm not sure if that would make a difference. I can't use normal variables as they reset on each render.
setProductPage will update the state and trigger a new render. productPage is not the state, it's just a variable holding the value of the state the moment you used useState. It will never change, it will always have the same value. You think it changes, but in reality, the function is simply executed again, but now productPage assigned with the new value.
So what can you do? You have two options.
The one it use useEffect, that will see if productPage changes (every time the function is executed = re-renders), and if it is, it will fetch the data and use setDisplayProducts to re-render the component with a new value in displayProducts. This is nice if you plan to have other ways of updating the productPage and you want them to also trigger the fetch.
const [productPage, setProductPage] = useState(1)
const [displayProducts, setDisplayProducts] = useState([])
const fetchProducts = async () => {
setProductPage(productPage + 1);
}
useEffect(() => {
const getDisplayProducts = async () => {
const newProductsData = await fetch(`/api/getproducts?page=${productPage}`)
const newProductsArr = await newProductsData.json()
setDisplayProducts(displayProducts.concat(newProductsArr))
};
getDisplayProducts();
}, [productPage]);
The second one is just store the new value in a variable, pass the variable to the setProductPage but use the variable in the fetch and not productPage.
const [productPage, setProductPage] = useState(1)
const [displayProducts, setDisplayProducts] = useState([])
const fetchProducts = async () => {
const nextProductPage = productPage + 1;
setProductPage(nextProductPage);
const newProductsData = await fetch(`/api/getproducts?page=${nextProductPage}`)
const newProductsArr = await newProductsData.json()
setDisplayProducts(displayProducts.concat(newProductsArr));
};
Setting a state is an async operation and you can't use it like a sync operation.
So, you must use useEffect hook, which runs after state changes.
import React, {useState, useEffect} from 'react';
const [productPage, setProductPage] = useState(1)
const [displayProducts, setDisplayProducts] = useState([])
const fetchProducts = async () => {
setProductPage(productPage + 1)
}
useEffect(() => {
const newProductsData = await fetch(`/api/getproducts?page=${productPage}`)
const newProductsArr = await newProductsData.json()
setDisplayProducts(displayProducts.concat(newProductsArr))
}, [productPage]); // <-- monitor productPage for changes

React/Redux How to set value from redux store to state of component

I need to change my received data from redux store to another variable (and then modify it).
At the moment, I receive data from store after API call and it is stored offices, but it is not set to my officeData variable. Does anyone how can I solve that?This is my code:
const dispatch = useDispatch();
const offices = useSelector((state) => state.office.offices)
const [officeData, setOffices] = useState(offices);
debugger;
useEffect(()=> {
dispatch(getOffices());
setOffices(offices);
debugger
}, [dispatch])
If you don't even enter your useEffect i think it's because you give dispatch as a dependency.
Effect are triggered when component mount but also when component update (prop or state). So you could do something like that :
import { useEffect } from "react";
const dispatch = useDispatch();
const offices = useSelector((state) => state.office.offices);
const [officeData, setOffices] = useState(undefined);
const [didMount, setDidmount] = useState(false);
// When component mount, load your Offices data
useEffect(() => {
if(!didMount){
dispatch(getOffices());
setOffices(offices);
} else {
setDidmount(true);
}
});
useEffect(() => {
if(didMount) {
// When you update officeData, do your thing
}
}, [officeData]);
I don't know the behavior of useSelector, but i guess it does not trigger a rendering. Maybe you could have a useEffect with offices as dependency, just be careful not to loop !

how can we use redux state in useState to set initial values

I am trying to use redux value to set an initial state of react component using useState.when I am trying to set the state of setIsStar it says currentChannelName is null. How can I avoid this? or is there any other way
const currentChannel = useSelector(state => state.channel.currentChannel);
const currentChannelName = currentChannel.name;
const [isStar, setIsStar] = useState({
[currentChannelName]: true
});
Possible solution is to combine it with useEffect:
const currentChannel = useSelector(state => state.channel.currentChannel);
const currentChannelName = currentChannel.name;
useEffect(() => {
if(currentChannelName) {
setIsStar({[currentChannelName]: true});
}
}, [currentChannelName]); // listen only to currentChannelName changes
const [isStar, setIsStar] = useState(currentChannelName ? {
[currentChannelName]: true
}: {});
`
You should avoid this as it dilutes your state across two separate domains.
Keep app-wide state in your redux store, keep local component state in your components.
If you really do want to do this, you'll probably just have to deal with the initial case where your component has mounted but the store is not populated with the data you need.
const currentChannel = useSelector(state => state.channel.currentChannel);
const currentChannelName = currentChannel.name;
const [isStar, setIsStar] = useState(currentChannelName && {
[currentChannelName]: true
});
The easiest solution would be:
// Redux state
const user = useSelector((state) => state.user);
const [firstName, setFirstName] = useState(user.firstName || '');

Resources