React Hook not setting with useEffect - javascript

I'm using useEffect to fetch some data from Trello and set some states. First I grab the card I'm looking for and call setCard and setCardLocation. Everything is working fine. Then I get into my else case and no matter what I do setPublishDate will never be set, the loop continues to run. Why do all of these other hooks work but my last one doesn't? Thanks.
export default function Home(props) {
const [performedFetch, setPerformedFetch] = useState(false);
const [slug, setSlug] = useState(null);
const [cardLocation, setCardLocation] = useState(1);
const [card, setCard] = useState(null);
const [publishDate, setPublishDate] = useState(null);
const key = ''; // imagine these are here
const token = '';
useEffect(() => {
setSlug(
new URLSearchParams(window.location.search).get('slug')
);
if (!performedFetch && !!slug) {
fetch(`https://api.trello.com/1/lists/${listId}/cards?key=${key}&token=${token}`)
.then(response => response.json())
.then(data => {
setPerformedFetch(true);
data.forEach((c, index) => {
if (c.desc.includes(slug)) {
setCard(c)
setCardLocation(index + 1)
} else if (!publishDate && index > cardLocation) {
console.log(publishDate); // why is this always null?? also runs multiple times
const name = c.name;
const frontHalf = name.split("/")[0].split(" ");
const month = frontHalf[frontHalf.length - 1];
const day = name.split("/")[1].split(")")[0];
setPublishDate(`${month}/${day}`);
}
});
});
}
});

As already mentioned by #TaghiKhavari, you should have two useEffects (Multiple effects to separate concerns).
Also, it is important to optimize the performance by skipping effects by providing a dependency array as second argument to the useEffect. So the effect will only re-run if any of its dependencies would change.
First effect for slug:
useEffect(() => {
setSlug(
new URLSearchParams(window.location.search).get('slug')
);
}, []) // Note: Remove "[]" if you want to set slug at each update / render Or keep it if you want to set it only once (at mount)
Second effect to fetch and set card and other details:
useEffect(() => {
if (!performedFetch && slug) {
fetch(
`https://api.trello.com/1/lists/${listId}/cards?key=${key}&token=${token}`
)
.then((response) => response.json())
.then((data) => {
setPerformedFetch(true)
// Note: if there can be only ONE matching card
const index = data.findIndex((card) => card.desc.includes(slug))
if (index > -1) {
const card = data[index]
setCard(card)
setCardLocation(index + 1)
const name = card.name
const frontHalf = name.split('/')[0].split(' ')
const month = frontHalf[frontHalf.length - 1]
const day = name.split('/')[1].split(')')[0]
setPublishDate(`${month}/${day}`)
}
// Setting State in a LOOP? is a problem
/*
data.forEach((card, index) => {
if (card.desc.includes(slug)) {
setCard(card)
setCardLocation(index + 1)
} else if (!publishDate && index > cardLocation) {
const name = card.name
const frontHalf = name.split('/')[0].split(' ')
const month = frontHalf[frontHalf.length - 1]
const day = name.split('/')[1].split(')')[0]
setPublishDate(`${month}/${day}`)
}
})*/
})
}
}, [slug, performedFetch])
Set states may be async to improve performance:
So, you should not set states in a loop as you are doing currently. If you must iterate through a loop and set all or few elements of the array in state, you can loop through the array and push all relevant items in a local array variable and set it to state after loop ends. Hope it helps!

It's because usually react states updates asynchronously and at the time you're checking for slug it hasn't set yet
you need to do something like this:
function Home(props) {
const [performedFetch, setPerformedFetch] = useState(false);
const [slug, setSlug] = useState(null);
const [cardLocation, setCardLocation] = useState(1);
const [card, setCard] = useState(null);
const [publishDate, setPublishDate] = useState(null);
const key = ""; // imagine these are here
const token = "";
useEffect(() => {
setSlug(new URLSearchParams(window.location.search).get("slug"));
});
useEffect(() => {
console.log(slug)
if (!performedFetch && !!slug) {
fetch(`https://api.trello.com/1/lists/${listId}/cards?key=${key}&token=${token}`)
.then(response => response.json())
.then(data => {
setPerformedFetch(true);
data.forEach((c, index) => {
if (c.desc.includes(slug)) {
setCard(c)
setCardLocation(index + 1)
} else if (!publishDate && index > cardLocation) {
console.log(publishDate); // why is this always null?? also runs multiple times
const name = c.name;
const frontHalf = name.split("/")[0].split(" ");
const month = frontHalf[frontHalf.length - 1];
const day = name.split("/")[1].split(")")[0];
setPublishDate(`${month}/${day}`);
}
});
});
}
}, [slug, performedFetch])
}

Related

How to get each item from a filtered array, without using a map for each item

I'm taking an array and filtering the value in a context:
const { responsible } = useResponsible()
const [ids, setIds] = useState([])
const filteredResponsible = responsible?.filter((resp) =>
ids.includes(resp.id)
)
The problem is that I need to make a map to get the corresponding value of each id, one by one. This ends up making the code too long:
const { filteredResponsible } = useResponsible
const responsibleName = filteredResponsible.map((resp) => resp.name)
const responsibleEmail = filteredResponsible.map((resp) => resp.email)
const responsibleAddress = filteredResponsible.map((resp) => resp.address)
...
And so on with each item in the array.
I'm using the React Hook Form's setValue to set the value in the inputs:
useEffect(() => {
setValue('name', `${responsibleName}`)
setValue('email', `${responsibleEmail}`)
setValue('address', `${responsibleAddress}`)
setValue('cep', `${responsibleCep}`)
setValue('district', `${responsibleDistrict}`)
setValue('city', `${responsibleCity}`)
setValue('state', `${responsibleState}`)
setValue('phone', `${responsiblePhone}`)
setValue('sex', `${responsibleSex}`)
}, [])
How can I make these maps smaller? Without having to make a map to get each item in the array?
There doesn't seem to be any reason to do those map calls on every render and to do them anywhere other than where you need them, since you only show using the result in a mount-only effect. Just do them there:
const { filteredResponsible } = useResponsible; // Is there really no `()` needed here?
useEffect(() => {
setValue("name", `${filteredResponsible.map(({name}) => name)}`);
setValue("email", `${filteredResponsible.map(({email}) => email)}`);
setValue("address", `${filteredResponsible.map(({address}) => address)}`);
// ...
}, []);
If you really need those distinct arrays on every render, unless you can change your data structures to be more amenable to your output I don't see you have a lot of options. You can at least avoid multiple loops through filteredResponsible:
const { filteredResponsible } = useResponsible; // ()?
const responsibleName = [];
const responsibleEmail = [];
const responsibleAddress = [];
for (const { name, email, address } of filteredResponsible) {
responsibleName.push(name);
responsibleEmail.push(email);
responsibleAddress.push(address);
}
And if that's really the case, you may want to avoid doing it on every render:
const { filteredResponsible } = useResponsible; // ()?
const { responsibleName, responsibleEmail, responsibleAddress } = useMemo(() => {
const responsibleName = [];
const responsibleEmail = [];
const responsibleAddress = [];
for (const { name, email, address } of filteredResponsible) {
responsibleName.push(name);
responsibleEmail.push(email);
responsibleAddress.push(address);
}
return { responsibleName, responsibleEmail, responsibleAddress };
}, [filteredResponsible]);

How to access variable inside a function in JavaScript?

I'm almost finishing my first project with React Native which is an expenditure app.
I'm building a chart from all my expenses in 2 currencies, the code is working I just have a small problem with accessing a local variable.
I would like to access the 2 sum variable inside this function, so I can use it in my return for React Native to build a chart.
// Get $ currency From fire base
const q = query(collection(db, "users"),where("selected", "==", "Gasto"), where("moneda", "==", "$"));
const unsubscribe = onSnapshot(q,(querySnapshot) => {
const dolarCurrency = [];
const months =[];
querySnapshot.forEach((doc) =>{
dolarCurrency.push(doc.data().cantidad);
months.push(doc.data().fecha)
})
const hash = months.map((Day) => ({ Day }));
const hashDolar = dolarCurrency.map(( Amount ) => ({ Amount }))
const output = hash.map(({Day},i) => ({Day, ...hashDolar[i]}));
/// I need this variable below
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
// Get C$ currency from Firebase
const q2 = query(collection(db, "users"), where("selected", "==", "Gasto"),where("moneda", "==", "C$"));
const unsubscribe2 = onSnapshot(q2, (querySnapshot) =>{
const localCurrency = [];
const months2 = [];
querySnapshot.forEach((doc) => {
localCurrency.push(doc.data().cantidad)
months2.push(doc.data().fecha)
})
const hash = months2.map((Day) => ({ Day }));
const hashLocalCurrency = localCurrency.map(( Amount ) => ({ Amount }))
const output = hash.map(({Day},i) => ({Day, ...hashLocalCurrency[i]}));
/// I need this variable below
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
Any help or advice is welcome and appreciated!
You can either try state or ref, depends on whether UI part is to be updated, if UI is to be updated then state else ref does the job
const [sum,setSum] = useState(0)
or
const sumRef = useRef(0)
Then after that
const sum = output.reduce((acc, cur)=> {
const found = acc.find(val => val.Day === cur.Day)
if(found){
found.Amount+=Number(cur.Amount)
}
else{
acc.push({...cur, Amount: Number(cur.Amount)})
}
return acc
}, [])
})
You can either setSum(sum) or sumRef.current = sum
Hope it helps , feel free for doubts

How to fix pagination first page load all data but onclick rest button load correctly in react

First part of my code:
const Inventory = () => {
const [products, setProducts] = useState([]);
const [pageCount,setPageCount] = useState(0);
//console.log(pageCount);
const [page,setPage] = useState(0);
const navigate = useNavigate();
useEffect(() => {
fetch(`http://localhost:3000/products?page=${page}`)
.then((res) => res.json())
.then((data) => setProducts(data));
}, [products,page]);
useEffect(()=>{
axios.get('http://localhost:3000/product-count')
.then((res) => {
const productCount = res?.data?.count;
//console.log("count product",count);
const pages = Math.ceil(productCount/6);
setPageCount(pages);
})
},[page])
Return section, this code will return pagination:
<div className="pagination">
{
[...Array(pageCount).keys()]
.map(number => <button key={number} className={page===number? 'selected': 'pagination'}
onClick={()=> setPage(number)}
>
{number+1}
</button>)
}
</div>
And this is the server-side code:
app.get("/products",async(req,res)=>{
const page = parseInt(req.query.page);
//console.log(page);
const q = {};
const cursor = productCollection.find(q);
let result;
if(page){
result = await cursor.skip(page * 6).limit(6).toArray();
}
else{
result = await cursor.toArray();
}
res.send(result);
})
// get product count
app.get("/product-count",async(req,res)=>{
const query = {};
const count = await productCollection.countDocuments(query);
res.json({count});
})
I want to load 6 data on the first load. But when data loads it displays all data. When I click the pagination button it works fine except the 1st button.
During the first load your front-end is sending page=0 to your backend.
In your server-side code you've the following statement: if (page)
But page will always be false when page=0, because 0 is a falsy value in javascript. So your statement always return the else block.
You need to change the statement to:
if (page !== undefined)

React-native-gifted-chat with cloud firestore pagination

I'm using Firestore to store messages. In order to optimize the mobile application performances, I would like to set a limit(50) in the firestore query.
It works well and implemented the onLoadEarlier React-native-gifted-chat available in the props.
All is working fine.
But, when I send a new message in the chat, after scrolled up to see the earliers messages, only the 50 last messages with the new one, off course, are available.
So, each time I'm adding a message in the Firestore database, the onSnapshot (in the useeffect) is executed and apply the limit query.
Is there a way to avoid this ?
Thanks.
Here my useEffect :
useEffect(() => {
const messagesListener = firestore()
.collection('groups')
.doc(group._id)
.collection('messages')
.orderBy('createdAt', 'desc')
.limit(50)
.onSnapshot(querySnapshot => {
const newMessages = querySnapshot.docs.map(doc => {
const firebaseData = doc.data();
const data = {
_id: doc.id,
text: '',
createdAt: new Date().getTime(),
...firebaseData
};
return data;
});
setMessages(previousMessages => {
return GiftedChat.append(previousMessages, newMessages);
});
});
return () => messagesListener();
}, []);
I am using FlatList in react-native to render chats and I had to paginate the chats list. Since Firestore query cursor is not supported in live listener, I created two list, recentChats & oldChats.
I populate recentChats using live listener query.onSnapshot & oldChats using cursor startAfter. FlatList data is combination of both list and I take care of merging logic.
const MESSAGE_LIMIT = 15;
const ChatWindow = props => {
const { sessionId, postMessage, onSendTemplateButtonPress } = props;
// Firestore cursor is not supported in query.onSnapshot so maintaining two chat list
// oldChats -> chat list via cursor, recentChats -> chat list via live listener
const [oldChats, setOldChats] = useState([]);
const [recentChats, setRecentChats] = useState([]);
// if true, show a loader at the top of chat list
const [moreChatsAvailable, setMoreChatsAvailable] = useState(true);
const [inputMessage, setInputMessage] = useState('');
useEffect(() => {
const query = getGuestChatMessagesQuery(sessionId)
.limit(MESSAGE_LIMIT);
const listener = query.onSnapshot(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
// merge recentChats & chats
if (recentChats.length > 0) {
const newRecentChats = [];
for (let i = 0; i < chats.length; i++) {
if (chats[i].sessionId === recentChats[0].sessionId) {
break;
}
newRecentChats.push(chats[i]);
}
setRecentChats([...newRecentChats, ...recentChats]);
} else {
setRecentChats(chats);
if (chats.length < MESSAGE_LIMIT) {
setMoreChatsAvailable(false);
}
}
});
return () => {
// unsubscribe listener
listener();
};
}, []);
const onMessageInputChange = text => {
setInputMessage(text);
};
const onMessageSubmit = () => {
postMessage(inputMessage);
setInputMessage('');
};
const renderFlatListItem = ({ item }) => {
return (<ChatBubble chat={item} />);
};
const onChatListEndReached = () => {
if (!moreChatsAvailable) {
return;
}
let startAfterTime;
if (oldChats.length > 0) {
startAfterTime = oldChats[oldChats.length - 1].time;
} else if (recentChats.length > 0) {
startAfterTime = recentChats[recentChats.length - 1].time;
} else {
setMoreChatsAvailable(false);
return;
}
// query data using cursor
getGuestChatMessagesQuery(sessionId)
.startAfter(startAfterTime)
.limit(MESSAGE_LIMIT)
.get()
.then(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
if (chats.length === 0) {
setMoreChatsAvailable(false);
} else {
setOldChats([...oldChats, ...chats]);
}
});
};
return (
<View style={[GenericStyles.fill, GenericStyles.p16]}>
<FlatList
inverted
data={[...recentChats, ...oldChats]}
renderItem={renderFlatListItem}
keyExtractor={item => item.messageId}
onEndReached={onChatListEndReached}
onEndReachedThreshold={0.2}
ListFooterComponent={moreChatsAvailable ? <ActivityIndicator /> : null}
/>
{
Singleton.isStaff ?
null:
<ChatInput
onMessageInputChange={onMessageInputChange}
onMessageSubmit={onMessageSubmit}
inputMessage={inputMessage}
style={GenericStyles.selfEnd}
onSendTemplateButtonPress={onSendTemplateButtonPress}
/>
}
</View>
);
};
Your query is OK for the first time, for consequent queries you must use the ::startAt or ::startAfter methods.
You can find more information in the official documentation.
https://firebase.google.com/docs/firestore/query-data/query-cursors

How to properly work with promises, Firebase and React's UseEffect hook?

Basically what I'm trying to achieve is to run some code only after a variable is not undefined anymore. However, I'm feeling very confused with the use of promises to await firebase responses, as well as with the use of React's useEffect hook.
I did some research that perhaps listeners are my answer to this? I couldn't really figure out how to implement them, though.
const weekday = moment().isoWeekday()
export const TimeGrid = () => {
const { userObject } = useContext(Context)
//(1) I wish to use the below to generate a list of times
const [LIST_OF_TIMES_FROM_DATABASE, SET_LIST_OF_TIMES_FROM_DATABASE] = useState([])
let businessId
function getBusinessId() {
if (userObject) businessId = userObject.businessId
}
getBusinessId()
//defines function that will run below inside of first useEffect
//also where the list of times is supposed to be set
function getWorkingHoursAccordingToDayOfTheWeek(day, snapshot) {
const workingHours = []
const ALL_AVAILABLE_TIMES = []
workingHours.push(snapshot.data().beginWeek)
workingHours.push(snapshot.data().endWeek)
const [begin, ending] = workingHours
let bgn = moment(begin, 'HH:mm')
const end = moment(ending, 'HH:mm')
let i = 0
while (bgn <= end) {
ALL_AVAILABLE_TIMES.push({
id: i,
type: 'time',
day: 'today',
time: bgn.format('HHmm'),
})
bgn = bgn.add(30, 'minutes')
i++
}
SET_LIST_OF_TIMES_FROM_DATABASE(ALL_AVAILABLE_TIMES) //(2) This is where I set the list, which won't work on its 1st run
}
useEffect(() => {
async function getTimesFromDB() {
const approved = await approvedBusinessService.doc(businessId)
const snapshotApproved = await approved.get()
if (snapshotApproved.exists) {
getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotApproved)
return
} else {
const pending = await businessPendingApprovalService.doc(businessId)
const snapshotPending = await pending.get()
if (snapshotPending.exists) {
getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotPending)
}
}
return
}
getTimesFromDB()
}, [userObject])
const allBookedTimes = allBookings.map(element => element.time)
//the below used to be 'listOfTimesJSON.map(item => item.time)' and used to work as it was a static JSON file
//(3) this is sometimes undefined... so all of the code below which relies on this will not run
const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)
const [counts, setCounts] = useState({})
useEffect(() => {
const counts = {}
//(4) below will not work as listOfTimes won't exist (because of 'LIST_OF_TIMES_FROM_DATABASE' being undefined)
listOfTimes.forEach(x => {
counts[x] = (counts[x] || 0) + 1
if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
})
setCounts(counts)
}, [])
const sortedListOfTimesAndCount = Object.keys(counts).sort() //(5) undefined, because 'counts' was undefined.
const displayListOfTimes = sortedListOfTimesAndCount.map(
time =>
time > currentTime && (
<>
<li>
{time}
</li>
</>
),
)
return (
<div>
<ul>{displayListOfTimes}</ul>
</div>
)
}
as per the comments in your code your steps 3 ,4 ,5 are dependent on LIST_OF_TIMES_FROM_DATABASE
and it's also not in sequence,
And you are expecting it to execute in the sequence,
1) First, it will execute all the thing which is outside the useEffect and it's dependency whenever there is change in state, so keep that in mind
2) useEffect(() => {...},[]) this will get called as soon as component mounts, so in most cases, you will get listOfTimes as [], because at that time API call might be in requesting data from server.
3) There are few unnecessary things like const [counts, setCounts] = useState({}) and useEffect(() => {...} , []), this is kind of overuse of functionality.
Sometimes you can do the things simply but with all these it becomes complicated
Solution :
So what you can do is just put all the code inside displayListOfTimes function and check for LIST_OF_TIMES_FROM_DATABASE if available then and only then proceed with steps 3,4,5. So you will never get LIST_OF_TIMES_FROM_DATABASE undefined
NOTE : And as per the code LIST_OF_TIMES_FROM_DATABASE either be blank
array or array with data but never undefined, and if you are getting undefined then you can check that while setting up the state
const weekday = moment().isoWeekday()
export const TimeGrid = () => {
const { userObject } = useContext(Context)
//(1) I wish to use the below to generate a list of times
const [LIST_OF_TIMES_FROM_DATABASE, SET_LIST_OF_TIMES_FROM_DATABASE] = useState([])
let businessId
function getBusinessId() {
if (userObject) businessId = userObject.businessId
}
getBusinessId()
//defines function that will run below inside of first useEffect
//also where the list of times is supposed to be set
function getWorkingHoursAccordingToDayOfTheWeek(day, snapshot) {
const workingHours = []
const ALL_AVAILABLE_TIMES = []
workingHours.push(snapshot.data().beginWeek)
workingHours.push(snapshot.data().endWeek)
const [begin, ending] = workingHours
let bgn = moment(begin, 'HH:mm')
const end = moment(ending, 'HH:mm')
let i = 0
while (bgn <= end) {
ALL_AVAILABLE_TIMES.push({
id: i,
type: 'time',
day: 'today',
time: bgn.format('HHmm'),
})
bgn = bgn.add(30, 'minutes')
i++
}
SET_LIST_OF_TIMES_FROM_DATABASE(ALL_AVAILABLE_TIMES) //(2) This is where I set the list, which won't work on its 1st run
}
useEffect(() => {
async function getTimesFromDB() {
const approved = await approvedBusinessService.doc(businessId)
const snapshotApproved = await approved.get()
if (snapshotApproved.exists) {
getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotApproved)
return
} else {
const pending = await businessPendingApprovalService.doc(businessId)
const snapshotPending = await pending.get()
if (snapshotPending.exists) {
getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotPending)
}
}
return
}
getTimesFromDB()
}, [userObject])
const displayListOfTimes = () => { // <----- Start - HERE
if(LIST_OF_TIMES_FROM_DATABASE && LIST_OF_TIMES_FROM_DATABASE.length) {
const counts = {}
const allBookedTimes = allBookings.map(element => element.time)
//(3) ------ this will never be undefined
const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)
//(4) ------ now it will work always
listOfTimes.forEach(x => {
counts[x] = (counts[x] || 0) + 1
if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
})
//(5) ------ now it will work
const sortedListOfTimesAndCount = Object.keys(counts).sort()
return sortedListOfTimesAndCount.map(
time =>
time > currentTime && (
<>
<li>
{time}
</li>
</>
),
)
} else {
return <li>Nothing to show for now</li>
}
}
return (
<div>
<ul>{displayListOfTimes}</ul>
</div>
)
}
The problem is because you useEffect which uses listOfTimes isn't being called when the listOfTimes value updates after a fetch request.
You can define listOfTimes within the useEffect and add a dependency on LIST_OF_TIMES_FROM_DATABASE
const [counts, setCounts] = useState({})
useEffect(() => {
// Defining it inside because if we don't and add allBookedTimes as a dependency then it will lead to an infinite look since references will change on each re-render
const allBookedTimes = allBookings.map(element => element.time)
const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)
const counts = {}
listOfTimes.forEach(x => {
counts[x] = (counts[x] || 0) + 1
if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
})
setCounts(counts)
}, [LIST_OF_TIMES_FROM_DATABASE, allBookings]); // Add both dependencies. This will ensure that you update counts state when the state for times changes

Categories

Resources