I am developing this application where I double click the page and create a new item. Then I notify a websockets server to broadcast this creation to all other users listening to it. On the other user end I must add this new item, so everybody gets synchronized.
I am using react hooks and this is my first app from the scratch using it. I am having several problems like this in this application, but I selected this flow to illustrate the basic problem. It must cover most issues.
When I call the useCallback
to add the item on the "other user" side, the items
state is old (empty). So no matter how many times I create items, I will only have one on the page, the one I just created.
This is a sample of the code:
import React, { useEffect, useState, useCallback } from 'react';
import io from 'socket.io-client';
let socket;
function App() {
const [items, setItems] = useState({});
const createItem = useCallback((data) => {
const id = generateId();
const newItem = { id, ...data };
const newItems = { ...items, [id]: newItem };
setItems(newItems)
// When I create a new item, I notify the server to broadcast it
socket.emit('itemCreated', newItem)
}, [items, setItems])
const doCreateItem = useCallback((item) => {
// When someone else creates an item, it should be invoked be invoked
const newItems = { ...items, [item.id]: item };
setItems(newItems)
}, [items]);
useEffect(() => {
// Connects to the server
socket = io('ws://localhost:8090', {
transports: ['websocket'],
path: '/ws'
});
// Listens to event
socket.on('createitem', (data) => {
doCreateItem(data)
});
// I cannot add doCreateItem here, otherwise it will call this effect everytime doCreateItem is executed
}, []);
return (
<div
onDoubleClick={
(e) => {
createItem({});
}
}
>
{
Object.values(items).map(item => {
return (
<div>{item.id}</div>
)
})
}
</div>
);
}
export default App;
The doCreateItem
has the items dependency. One the other hand, the useEffect
dependency list doesn't contain doCreateItem
mostly because it will reinvoke the useEffect
callback everytime I receive a new item from the server. And also because I want this effect to be executed only once (on component mounting). I tried splitting the useEffect
in two, one for the connection and another for the listeners, but the same happens.
I actually had several problems like this one. A callback is executed more than I was expecting or the state is not updated on the callback.
Could someone clarify how we should get around this problem?
Thanks in advance!
Edit
As suggested by @Ross, I changed the setItems
calls to receive a callback and return the new state. That helped with the setItems
dependency. But I still can't read items
in the doCreateItem
function. It is always empty.
Something funny that I noticed, is that if I stop using the useCallback
and recreate the functions on each render, it doesn't have the updated state on that function, but it does have outside the function:
function App() {
const [items, setItems] = useState({});
console.log('Rendered again and knows the state >', items)
const doCreatePostIt = (data) => {
console.log('Empty >', items)
// ...
};
// ...
}
When the next state depends on the current one, using the functional update form of useState
eliminates a dependency in the dependency array and solves the "stale state" problem.
setX
functions returned from the useState
hook is stable and does not change between renders (see useState
doc); it is safe to omit from dependency arraysconst [items, setItems] = useState({});
const createItem = useCallback((data) => {
const id = generateId();
const newItem = { id, ...data };
setItems((currItems) => ({
...currItems,
[id]: newItem,
}))
socket.emit('itemCreated', newItem)
}, [])
useEffect(() => {
function doCreateItem(item) {
setItems((currItems) => ({
...currItems,
[item.id]: item,
}))
}
socket = io('ws://localhost:8090', {
transports: ['websocket'],
path: '/ws'
});
socket.on('createitem', doCreateItem);
// Return a clean up function so the component will stop listening to the
// socket when the component is unmounted.
return () => {
socket.off('createItem', doCreateItem)
}
}, []);