I have been trying to figure something out in my React cryptocurrency project. So I have a function that basically polls for a response from the api every 6 seconds. It is supposed to start polling the moment the component renders, so I have it in a useEffect. It has default parameters being passed in for the polling to happen right after render. The polling works in terms that it polls and get the response based on the default values. It also takes in the user inputs and returns a response based on it, however, after the next poll, the arguments being passed to the api for polling are back to their defaults. In other words, the arguments being passed into the polling function on the next polls are all back to the default values I passed in, and not a continuation of the current state of the arguments.
Here is where I define pair, which lives above the useEffect:
const baseAsset = transactionType === TRANSACTION_TYPES.BUY ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const quoteAsset = transactionType === TRANSACTION_TYPES.SELL ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
Here is the function that gets polled:
const handleInitPoll = useCallback((baseAndQuote, side, value) => {
setIsLoading(true);
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}, [pair, transactionType, fromCurrencyAmount]);
And here is the useEffect that polls it:
useEffect(() => {
if (isLoggedIn) {
getSwapPairs()
.then((res) => {
setSwapInfo(res.markets);
// Set state of selected currencies on render
if (transactionType === TRANSACTION_TYPES.BUY) {
setSelectedCurrencyState({
...selectedCurrencyState,
selectedToCurrency: (quoteAsset !== symbol) ? symbol : ''
});
}
});
if (symbol === selectedCurrencyState.selectedToCurrency) {
actions.updateChartQuote(selectedCurrencyState.selectedFromCurrency);
}
if (pair && transactionType && symbol === baseAsset) {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
}
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'SHIB', selectedToCurrency: 'DASH' });
}, [pair, transactionType, fromCurrencyAmount, symbol]);
Here is the debouncing method:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the user is entering the input to debounce the api call when a user input is detected onChange:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
You should put pair in a useState hook. otherwise, everytime when this component is redenrering. the following part will be executed:
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
That's why you got a initial value again.
useState can help you to preserve the value for the whole component lifecircle unless you call set***().
Related
The if statement in canBookSlot() is only checked once for some reason. The second time canBookSlot() is triggered, the userDetailsObj.canBook should be 0 after running updateUser(). And according to the console log it is the case, but the if statement still runs, why?
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const updateUser = () => {
userDetailsObj["hasBooked"] = 1;
userDetailsObj["canBook"] = 0;
};
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
After each render userDetailsObj will take that value from localStorage. That's how every variable inside a component which isn't a state made with useState hook, or a ref made with useRef hook behaves. You can fix your problem this by using a state, like so:
const [userDetails, setUserDetails] = useState(JSON.parse(localStorage.getItem("userDetails")));
const updateUser = () => {
const newUserDetails = { ...userDetailsObj, hasBooked: 1, canBook: 0 };
setUserDetails(newUserDetails);
localStorage.setItem("userDetails", JSON.stringify(newUserDetails));
};
const canBookSlot = (id) => {
if (userDetails.canBook != 0) {
//Always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
Can you clarify where this code runs? Are you using a class component or functional component? Would you mind sharing the entire component? If it is doing what I think it is doing, the let userDetailsString = localStorage.getItem("userDetails"); is running every render which means on every render, it grabs the value in localStorage and uses that, rather than using your object stored in userDetailsObj.
If you are using functional components, you could fix this by using state.
let userDetailsString = localStorage.getItem("userDetails");
let [userDetailsObj, updateUserDetailObj] = useState(JSON.parse(userDetailsString));
const updateUser = () => {
let u = { ...userDetailsObj,
hasBooked: 1,
canBook: 0,
}
updateUserDetailObj(u);
};
If you are using class Components, let me know and I'll update it with that option.
I am on the newer side of React and trying to change the state of an object in an array. Currently, I am pulling the object out of the array, changing the property in that object, then adding the new object to the state again. Problem being that it sends the object to the back of the list and reorders my checkbox inputs.
const handleChange = (e) => {
if (e.target.type === "checkbox") {
// Get the role from the current state
const roleToChange = input.roles.find(
(role) => Number(role.id) === Number(e.target.id)
);
// Change checked state to opposite of current state
const changedRole = { ...roleToChange, checked: !roleToChange.checked };
// Get every role except the one that was changed
const newRoles = input.roles.filter(
(role) => Number(role.id) !== Number(e.target.id)
);
// Update the role in the state
setInput((prevState) => {
return { ...prevState, roles: [...newRoles, changedRole] };
});
}
Can I update the object in the array in-place so this doesn't happen?
Don't .filter - .map instead, and return the changed object in case the ID matches, so the new object gets put at the same place in the new array as it was originally.
const handleChange = (e) => {
if (e.target.type !== "checkbox") {
return;
};
const newRoles = input.roles.map((role) =>
Number(role.id) !== Number(e.target.id)
? role
: { ...role, checked: !role.checked }
);
setInput((prevState) => {
return {
...prevState,
roles: newRoles
};
});
}
Unless the state is updated synchronously before this, which sounds a bit unlikely (but not impossible), you can also probably use setInput({ ...input, roles: newRules }) instead of the callback.
I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})
I am developing a React Native application and am facing the following error:
I have defined a useRef which stores the doc ID from a firebase collection. But when I call that variable after it has been defined, the .current value returns a blank string.
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
console.log(bidId.current)
}
})
})
The above code returns the expected value. However, when I call the variable outside this db.collection loop, I get the following value:
But calling the bidId.current returns a blank string.
Please can someone help me with this. Thanks!
Actually this is what happens:
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
// This line gets executed after some time!
console.log(bidId.current)
}
})
})
// This gets executed first! (The value has not been stored yet!)
console.log(bidId.current);
Using the "useState" hook instead of "useRef" will solve the issue. Consider the following code:
const [BidId, setBidId] = useState<string | null>(null);
// This will store the value...
useEffect(() => {
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
setBidId(doc.id);
}
})
})
}, []);
// Here you can access the value
useEffect(() => {
if(BidId !== null)
console.log(BidId);
}, [BidId]);
// You can also return the component like the following:
return (<View>The Bid ID is: {BidId !== null ? BidId : "Loading..."}</View>);
Your useEffect basically says that whenever pageRef changes, call this function. If done outside, it will call do your tasks on every render instead of doing the whenever pageRef values is changed. Also, in initial renders, it may give undefined values.
You can only return a function in useEffect which basically says that before running the same next time, run this function before.
Try (currentUser without the '?' query character):
if (doc.data().email === auth.currentUser.email) {
bidId.current = doc.id
console.log(bidId.current)
}
Problem
My component renders every second instead of only rendering if the state passed into setCurrentlyPlaying() is different. How can I fix this behavior?
Background
I would like to check an API every 1 second. Aside from the first render, I only want my component to rerender when the incomingSong differs from the currentlyPlayingSong. I'm banking on the State hook's ability to only trigger a rerender if the passed in state is different than the previous based on the Object.is comparison algorithm. Even if I only return(<div></div>); in my component it continues to render every 1 second, so it seems like the problem stems from this logic. I test this by playing NOTHING on my Spotify so it always goes into the first if statement and it still rerenders every second (prints the console.log("RENDERED") statement).
const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({ type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''});
console.log("RENDERED")
useEffect(() => {
console.log("initial effect including updateSongs");
const updateSongs = async () => {
const result = await getCurrentlyPlaying();
var incomingSong = {};
// no song is playing
if(result.data === "") {
incomingSong = { type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''};
}
// some song is playing
else {
var songJSONPath = result.data.item;
incomingSong = {
type: 'Now Playing',
name: songJSONPath.name,
id: songJSONPath.id,
artists: songJSONPath.artists.map(artist => artist.name + " "),
imageUrl: songJSONPath.album.images[songJSONPath.album.images.length - 2].url,
};
}
// console.log("currentlyPlayingSong: ", currentlyPlayingSong)
// console.log("incomingSong: ", incomingSong);
// this line!!!!
setCurrentlyPlayingSong(incomingSong);
}
updateSongs();
const timer = setInterval(updateSongs, 1000);
return () => clearInterval(timer);
},[]);
Console logging in the body of a functional component is not actually an accurate measure for how often a component is rendered to the DOM, you should do this in an useEffect hook.
Also, updateSongs creates a new incomingSong object reference each time it's invoked, and since react uses shallow reference equality it will trigger a rerender every time you setCurrentlyPlayingSong(incomingSong);. You should do this conditional check manually before updating state with a new value. How you determine equality between the objects is up to you. id seems a good choice.
Check if the song id is different and only update state when it is actually a different song.
if (currentlyPlayingSong.id !== incomingSong.id) {
setCurrentlyPlayingSong(incomingSong);
}
code
const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({
type: 'Now Playing',
name: 'No Song Playing',
artists: [],
imageUrl: '',
});
useEffect(() => {
console.log("RENDERED"); // <-- logs when component is rendered to DOM
});
useEffect(() => {
console.log("initial effect including updateSongs");
const updateSongs = async () => {
const result = await getCurrentlyPlaying();
let incomingSong = {};
// no song is playing
if (result.data === "") {
...
}
// some song is playing
else {
...
}
// check if song id is different and only update if true
if (currentlyPlayingSong.id !== incomingSong.id) {
setCurrentlyPlayingSong(incomingSong);
}
}
updateSongs();
const timer = setInterval(updateSongs, 1000);
return () => clearInterval(timer);
},[]);
Assuming a song's name indicates the identity of a song. You can add an if statement before updating the state.
if (currentlyPlayingSong.name !== incomingSong.name) {
setCurrentlyPlayingSong(incomingSong);
}
This will make sure to update the state only when the song has changed. If name isn't the correct property to check then replace it with the correct one.
React has no way of understanding that it can prevent the state update as you're passing a new object every time. See this to know more about react compares different values.