I have a form with a single input that takes some value, sends a request to the backend to create a new entity and updates the frontend by adding the new value to the collection of items.
Handler which gets executed on form submit:
const handleItemInsert = async (item: any): Promise<void> => {
console.log('BEFORE ITEM CREATE', items);
const newItem: IItem = await service.addNewItemAsync(item);
console.log('AFTER ITEM ADD', items);
console.log(newItem);
setItems((items) => [...items, newItem]);
}
My issue is that the first time I add a new item the state is somehow modified(the newItem is added to the items collection) before even the service gets called. Furthermore, the new item gets added twice and I am not sure why. Every related function that I've checked is called once and I have this problem with and without using StrictMode, so the issue shouldn't be based on that.
Here is a full Codesandbox sample of the issue.
I've put logs in the function calls that reflect the problem in the flow(you can check them in the codesandbox console window). I might be missing something really trivial but at this point I really don't see where the error lies.
Also, if the problem lies in the handling of the form submit itself through antd's onFinish method(as I am passing an async method), it should still execute update logic after the logs and don't add the item twice.
Any help will be appreciated.
Since your service is mutating ITEMS and you set that value to the state, you are inadvertently modifying React's state.
Update you effect to create a shallow copy of items when you set them:
useEffect(() => {
async function fetchItems() {
console.log("FETCH ITEMS CALLED.");
const items = await service.getAllItemsAsync();
console.log("INITIAL ITEMS", items);
setItems([...items]);
}
fetchItems();
}, []);
Related
On a page load I have two useEffects. Both are executing at load, where the first one can possibly set a state, that should trigger the second useEffect one further time. But actually it won't.
Actually it should trigger, as it executes in two cases: When i change the order of these useEffects (could be a solution, but why???), or when i comment out the void getOrPostOnWishlist();, thus when removing the async call from the useEffect. But why is that a problem here?
Here some example code snippet with some comments:
...
const setItemIdToBeHandled = (itemId: number | undefined) =>
setState((prevState) => ({...prevState, itemIdToBeHandled: itemId}));
...
// async, called on second useEffect
const getOrPostOnWishlist = async () => {
if (state.itemIdToBeHandled) {
// if there is an item to be handled, retrieve new wishlist with added item
await addItemToNewWishlist(state.itemIdToBeHandled);
} else if (!state.wishlist) {
// if no wishlist is locally present, check if wishlist exists on api
await checkForPresentWishlist();
}
};
// possibly setting state
React.useEffect(() => {
const urlItemId = UrlSearchParamsHelper.wishlistItemId;
if (urlItemId) {
console.log("found item id in url:", urlItemId);
setItemIdToBeHandled(urlItemId);
}
}, []);
// on state change, but also on load
React.useEffect(() => {
console.log("condition:", state.itemIdToBeHandled); // sticks on 'undefined'
void getOrPostOnWishlist(); // when commented out, above console will show 'undefined', and then an itemId (considering the first useEffect sets the state);
}, [state.itemIdToBeHandled]);
This led to the following output:
But when just commenting out the async call in the second useEffect, this led to:
Googled around, and also tried useCallback, but that didn't work. Doesn't seem to be the issue here, since it's somewhat not about the content of the called function, but about the very fact, that the calling useEffect is not even executed.
It feels like even without await inside the useEffect, a useEffect is still blocked, when it has executed an async function.
Or am i missing something? If some more details are needed, let me know
Intro
I'm creating a chat application and was trying to get rid of useless messages rerenders in a messages list when requesting them with onValue from firebase (snapshot from onValue was overwriting whole messages list state which caused to a rerender of all messages). Solution i came up with is to use onChildAdded which
is triggered once for each existing child and then again every time a
new child is added
I created a useList hook which supposed to return a list of messages on every change of chatId (when switching between chats)
export function useList(chatId) {
const [list, setList] = useState([]);
const database = getDatabase();
useEffect(() => {
setList([])
const chatQuery = query(
ref(database, "/messages/" + chatId),
orderByValue("timestamp")
);
onChildAdded(chatQuery, (data) => {
setList((prev) => [...prev, { key: data.key, message: data.val() }]);
});
}, [chatId]);
return [list];
}
And it works fine, but... :(
Problem
When i'm changing chatId few times (was in a chat with person A, then switched to the person B, and then back to A) and sending message to A, this last message is displayed twice in a list of messages with a two children with the same key
error.
Moreover if im switching between chats more then twice, and then sending a message, the state of this last message seems to be accumulating, and it is displayed as many times as there were switches between chats. All messages are sended to the firebase properly, after page reload everything is fine, no duplicates of last message, only after switching between chats.
Conclusion
I'm fairly new in a react and firebase so, if there is some better way of solving all messages rerenders i would be happy to know about it. Also i would really appreciate explanation of this bug and it's solution :)
My Messages list
function Messages() {
const chatId = useSelector((state) => state.chat.id);
const [messages] = useList(chatId);
return (
<VStack w="100%" h="100%" p="5" overflowY="scroll" spacing="20px">
{messages.map(({ key, message }) => {
return <Message key={key} message={message}/>;
})}
</VStack>
);
}
4 Solution
useEffect(() => {
setList([]);
onChildAdded(chatQuery, (data) => {
setList((prev) => [...prev, { key: data.key, message: data.val() }]);
});
return () => off(chatQuery, 'child_added');
}, [chatId]);
Once you call onChildAdded for a query/path that callback will be called for new messages until you explicitly unsubscribe from updates. Since you only ever call onChildAdded, you're adding more and more subscriptions.
So you'll want to stop the listener as shown in the documentation on detaching listeners, or by calling the unsubscribe function that onChildAdded returns. If I remember correctly how lifecycle management in useEffect hooks works, you can return the function that is used to unsubscribe. So it could be as simple as returning the function that you get back from onChildAdded.
I'm currently trying to fetch all of the properties for an object from an API, and display them in a table. The API will return up to 10 results at a time, and will return a value nextPageToken in the response body if there are more results to be fetched. My goal is to fetch the first 10 results, immediately display them in the table, and add to the table as I continue to hit the API. This was my first attempt at a solution:
const getProperties = async (id) => {
const properties = await Api.getProperties(id);
setProperties(properties.properties);
if (properties.nextPageToken) loadMoreProperties(id, nextPageToken);
};
const loadMoreProperties = async (id, nextPageToken) => {
const properties = await Api.getProperties(id, nextPageToken);
setProperties(prevProperties => {return [...prevProperties, properties.properties]});
if (properties.nextPageToken) loadMoreProperties(id, properties.nextPageToken);
};
(Note that the above is a simplification; in practice, there's more logic in getProperties that doesn't need to be repeated on subsequent calls to the API)
The problem that I'm running into with this solution is that when I'm calling loadMoreProperties, the setProperties call isn't yet finished. How can I enforce that the call to loadMoreProperties only happens after setting the previous set of properties? Is there an overall better pattern that I can follow to solve this problem?
You can use useEffect to trigger the page loads as a reaction to a completed state change:
const [page, setPage] = useState(); // will be {properties, nextPageToken}
// load first page whenever the id changes
useEffect(() => {
Api.getProperties(id)
.then(page => setPage(page)));
}, [id]);
// load next page (if there is one) - but only after the state changes were processed
useEffect(() => {
if (page?.nextPageToken == null) return;
Api.getProperties(id, page.nextPageToken)
.then(page => setPage(page)));
}, [id, page]
);
// this will trigger the re-render with every newly loaded page
useEffect(()=> setProperties(prev => [...(prev || []), page.properties]), [page]);
The first effect will cause an update to the state variable page.
Only after the state change is completed, the second effect will be triggered and initiate the fetch of the second page.
In parallel, the third effect will perform the changes to the state variable properties, that your table component depends on, after each successful page load and page state update, triggering a re-render after each update.
I think you should pass a callback parameter to your "setProperties" method, to make the second call after the value has been updated, like this :
setProperties(properties.properties, () => {
if (properties.nextPageToken)
loadMoreProperties(id, nextPageToken);
);
Hope it can help
My solution involves removing the loadMoreProperties method itself.
While calling the getProperties for the 1st time, you can omit the nextPageToken argument.
getProperties = async(id,nextPageToken) {
var result = await Api.getProperties(id,nextPageToken);
this.setState((state)=>(state.properties.concat(result.properties)), ()=>{
// setState callback
if(result.nextPageToken) {
this.getProperties(id, nextPageToken);
}
});
}
I have a list of items stored in state. Upon form submission i add another item to the list and then save that as the new state. This newly added item has the status "pending." At the same time i send a post request and if the post request fails i want to update that particular items status to "error". The problem is, the state isn't updated by the time the request fails, and so i'm trying to update a state that isn't set.
I am using react hooks, so one possibility is to call the request only after the state has updated:
useEffect = (()=>{
function getRequest(URL, id, freq) {
request happens here
}
}),[state])
Previously, before putting the getRequest function in useEffect, it was called by another function in a child component.
My question consists of several parts:
1) how do i get the parameters URL, id, freq into the useEffect function?
2) i don't want to run the getRequest function on first render, so how can i negate this?
3) is my general pattern of doing things here good (i'm sure it shouldn't be this difficult).
If you want to make sure you're updating an item that was already added to the state, then indeed the way to make this synchronously is to use useEffect.
However, I added an extra state variable, which represents the amount of items.
Then, whenever a new item is added, the API request will be executed and the (existing) item will be updated accordingly:
const [itemsAmount, setItemsAmount] = setState(0);
const [items, setItems] = setState([]);
function addItem(itemData) {
const newItem = {
...itemData,
status: 'pending',
}
setItems([...items, newItem]);
setItemsAmount(itemsAmount + 1);
};
useEffect(async () => {
try {
// Execute the API call
const response = await apiCall();
if (resposne && response.data) {
// Add the successfully updated version of the item
newItem.status = 'done';
newItems.data = response.data;
setItems([...items, newItem]);
}
} catch (err) {
// If the API call fails, add the "failed" version of the item
newItem.status = 'failed';
setItems([...items, newItem]);
}
}, [itemsAmount]);
I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},