Adding data in React component via websocket - reactjs

I'm trying to add a data to a table of some operations in React via WebSockets. I even get the new data from WebSocket successfully. I stuck with the question how to add a new data to existing. When I recieve new data in websocket response, my const operationList becomes empty.
Have a look at my code:
const [operationsList, setOperationsList] = useState([{}] )
// Here I get the existing data from backend API and store to operationsList. It works
async function fetchOperations(activeID) {
if (activeID !== false) {
const response = await axios.get(
`http://127.0.0.1:8000/api/operations/?made_by=${activeID}`
)
setOperationsList(response.data)
}
}
useEffect(() => {
setIsOperationsLoading(true)
fetchOperations(activeID)
.then(() => {setIsOperationsLoading(false)})
.catch((e) => {setOperationsError(e)})
},[activeID])
// Here I subscribe to websockets to get new data for adding to operationsList
useEffect(() => {
const ws = new WebSocket('ws://127.0.0.1:8000/ws/')
ws.addEventListener('message', (e) => {
const response = JSON.parse(e.data)
console.log(response) // Here I see new data. It's ok
console.log(operationsList) // All of the sudden operationsList become empty
})
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe_to_operations_activity",
request_id: new Date().getTime(),
}))
}
}, [])
I thought that in my second useEffect I could just add response data from WebSocket like
setOperationsList([response, operationsList]). But operationsList is empty, so I've got just a new data in the table. How to fix it?

The second useEffect hook runs only once when the component mounts, you are logging the initial operationsList state value closed over in callback scope. In other words, it's a stale enclosure over the operationsList state.
I'm guessing it's at this point you are wanting to append response to the operationsList state. You can use a functional state update to correctly access the previous state and append to it.
You may also want to unsubscribe to the "message" event in the useEffect hook's cleanup function. This is to prevent resource leaks and attempts to update state of unmounted components.
useEffect(() => {
const ws = new WebSocket('ws://127.0.0.1:8000/ws/');
const handler = (e) => {
const response = JSON.parse(e.data);
setOperationsList(operationsList => [
...operationsList, // <-- shallow copy previous state
response, // <-- append new data
]);
};
ws.addEventListener('message', handler);
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe_to_operations_activity",
request_id: new Date().getTime(),
}));
};
return () => {
ws.removeEventListener('message', handler);
};
}, []);

Related

React state is never up to date in my callback

In a react functional component, I setState from an api call. I then create an EventSource that will update the state each time an event is received.
The problem is that in the EventSource callback, the state is not updated.
My guess is, state is printed at the creation of the callback, so it can't be updated.
Is there any other way to do it ?
function HomePage() {
const [rooms, setRooms] = useState<Room[]>([]);
const getRooms = async () => {
const data = await RoomService.getAll(currentPage);
setRooms(data.rooms);
}
const updateRooms = (data: AddRoomPayload | DeleteRoomPayload) => {
console.log(rooms); // rooms is always empty
// will append or remove a room via setRooms but i need actual rooms first
}
useEffect(() => {
// setState from api response
getRooms();
// set up EventSource
const url = new URL(process.env.REACT_APP_SSE_BASE_URL ?? '');
url.searchParams.append('topic', '/room');
const eventSource = new EventSource(url);
eventSource.onmessage = e => updateRooms(JSON.parse(e.data));
}, [])
...
}
Try using a functional update when using setRooms like this:
const updateRooms = (data: AddRoomPayload | DeleteRoomPayload) => {
setRooms((rooms) => {
if (data.type === 'add') {
return [...rooms, data.room];
} else if (data.type === 'remove') {
return rooms.filter(/* ... */);
}
});
}
Here is a reference to the React Docs on functional updates in useState: https://reactjs.org/docs/hooks-reference.html#functional-updates
If that doesn't work then try checking the React Developer Tools to make sure that the component's state rooms is being updated.

Updating the state correctly after fetch data from firestore

I am trying to use Firestore Snapchats to get real time changes on a database. I am using react-native-cli: 2.0.1 and react-native: 0.64.1 .
export const WelcomeScreen = observer(function WelcomeScreen() {
const [listData, setListData] = useState([]);
const onResult = (querySnapshot) => {
const items = []
firestore()
.collection("Test")
.get()
.then((querySnapshot) => {
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data}
items.push(tempData);
});
setListData(items)
});
}
const onError = (error) => {
console.error(error);
}
firestore().collection('Test').onSnapshot(onResult, onError);
}
Every thing is working perfectly, until I use setListData to update the data list. The App does not respond anymore and I get a warning message each time I try to add or delete data from the database
Please report: Excessive number of pending callbacks: 501. Some pending callbacks that might have leaked by never being called from native code
I am creating a deadlock by setting the state this way?
First, you don't want to set up a snapshot listener in the body of your component. This results in a growing number of listeners, because every time you render you add a new listener, but every listener results in rendering again, etc. So set up the listener just once in a useEffect:
const [listData, setListData] = useState([]);
useEffect(() => {
function onResult(querySnapshot) {
// ...
}
function onError(error) {
console.log(error);
}
const unsubscribe = firestore().collection('Test').onSnapshot(onResult, onError);
return unsubscribe;
}, []);
In addition, your onResult function is going to get called when you get the result, and yet you're having it turn around and immediately doing a get to re-request the data it already has. Instead, just use the snapshot you're given:
function onResult(querySnapshot) {
const items = []
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data()}
items.push(tempData);
});
setListData(items);
}

how to cancel useEffect subscriptions of firebase

I'm not quite understand how useEffect cleanup function work. Because whatever I do I always get warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Here is my code:
useEffect(() => {
setLoading(true)
// Get position list
const getPositionList = db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
})
return () => getPositionList
}, [])
useEffect expects as a return value an unsubscribe/cleanup function, or a null. Your .get() is NOT a subscription, so there's nothing to clean up
BUT useEffect is NOT asynchronous, and the .get() definitively returns a promise that needs a delay, even with the .then(). Your dependency array is empty, so useEffect is only called once - but I suspect your component unmounted before the .get() ever returned.
Your code will need to be closer to:
useEffect(() => {
setLoading(true)
(async () => {
// Get position list
await db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
});
)();
return null
}, [])
the ( async () => {})() creates an anonymous asynchronous function, then executes it. You do want to try to structure your system such that the enclosing component does not unmount before loading is set to false - we don't see that here, so we have to assume you do that part right.
The important take-aways:
useEffect is NOT asynchronous
.get() IS asynchronous
( async () => {}) creates an anonymous async function, and ( async () => {})() creates a self-executing anonymous asynchronous function
useEffect(() => {
setLoading(true)
// below firebase api return promise. hence no need to unsubscribe.
db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
setPositionsList(data.list)
setLoading(false)
})
return () => {
// any clean up code, unsubscription goes here. unmounting phase
}
}, [])
According #Dennis Vash answer I figure out, that before setting state I have to check is component mounted or not, so I add variable in useEffect and before set state I check I add if statement:
useEffect(() => {
setLoading(true)
let mounted = false // <- add variable
// Get position list
db.collection('lists').doc('positions').get()
.then( res => {
let data = JSON.stringify(res.data())
data = JSON.parse(data)
if(!mounted){ // <- check is it false
setPositionsList(data.list)
setLoading(false)
}
})
return () => {
mounted = true // <- change to true
}
}, [])

What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?

I am working on a react chap app that pulls data from a firebase database. In my "Dashboard" component I have an useEffect hook checking for an authenticated user and if so, pull data from firebase and set the state of a an email variable and chats variable. I use abortController for my useEffect cleanup, however whenever I first log out and log back in I get a memory leak warning.
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by Context.Consumer)
Originally I didn't have the abortController, I just returned a console log on clean up. Did more research and found abortController however the examples use fetch and signal and I could not find any resources on using with async/await. I am open to changing how the data is retrieved, (whether that is with fetch, async/await, or any other solution) I just have not been able to get it working with the other methods.
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
const abortController = new AbortController();
firebase.auth().onAuthStateChanged(async _user => {
if (!_user) {
history.push('/login');
} else {
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(async res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
await setEmail(_user.email);
await setChats(chatsMap);
});
}
});
return () => {
abortController.abort();
console.log('aborting...');
};
}, [history, setEmail, setChats]);
Expected result is to properly cleanup/cancel all asynchronous tasks in a useEffect cleanup function. After one user logs out then either the same or different user log back in I get the following warning in the console
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions
and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by Context.Consumer)
In the case of firebase you aren't dealing with async/await but streams. You should just unsubscribe from firebase streams in cleanup function:
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
let unsubscribeSnapshot;
const unsubscribeAuth = firebase.auth().onAuthStateChanged(_user => {
// you're not dealing with promises but streams so async/await is not needed here
if (!_user) {
history.push('/login');
} else {
unsubscribeSnapshot = firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
setEmail(_user.email);
setChats(chatsMap);
});
}
});
return () => {
unsubscribeAuth();
unsubscribeSnapshot && unsubscribeSnapshot();
};
}, [history]); // setters are stable between renders so you don't have to put them here
The onSnapshot method does not return a promise, so there's no sense in awaiting its result. Instead it starts listening for the data (and changes to that data), and calls the onSnapshot callback with the relevant data. This can happen multiple times, hence it can't return a promise. The listener stays attached to the database until you unsubscribe it by calling the method that is returned from onSnapshot. Since you never store that method, let alone call it, the listener stays active, and will later again call your callback. This is likely where the memory leak comes from.
If you want to wait for the result from Firestore, you're probably looking for the get() method. This gets the data once, and then resolves the promise.
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res => {
One way to cancel async/await is to create something like built-in AbortController that will return two functions: one for cancelling and one for checking for cancelation, and then before each step in async/await a check for cancellation needs to be run:
function $AbortController() {
let res, rej;
const p = new Promise((resolve, reject) => {
res = resolve;
rej = () => reject($AbortController.cSymbol);
})
function isCanceled() {
return Promise.race([p, Promise.resolve()]);
}
return [
rej,
isCanceled
];
}
$AbortController.cSymbol = Symbol("cancel");
function delay(t) {
return new Promise((res) => {
setTimeout(res, t);
})
}
let cancel, isCanceled;
document.getElementById("start-logging").addEventListener("click", async (e) => {
try {
cancel && cancel();
[cancel, isCanceled] = $AbortController();
const lisCanceled = isCanceled;
while(true) {
await lisCanceled(); // check for cancellation
document.getElementById("container").insertAdjacentHTML("beforeend", `<p>${Date.now()}</p>`);
await delay(2000);
}
} catch (e) {
if(e === $AbortController.cSymbol) {
console.log("cancelled");
}
}
})
document.getElementById("cancel-logging").addEventListener("click", () => cancel())
<button id="start-logging">start logging</button>
<button id="cancel-logging">cancel logging</button>
<div id="container"></div>

React state hook doesn't properly handle async data

I'm trying to set a component's state through an effect hook that handles the backend API. Since this is just a mock, I'd like to use the vanilla react methods and not something like redux-saga.
The problem is that while the fetching part works, the useState hook doesn't update the state.
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(data)
console.log(odds, data) // {}, {...actual data}
})()
}, [])
I've tried to pipe the whole process on top of the fetch like
fetch('odds')
.then(res => res.json())
.then(data => setOdds(data))
.then(() => console.log(odds)) // is still {}
But it doesn't make a single difference.
What am I doing wrong?
Basically if you call setOdds, the value of odds does not change immediately. It is still the last reference available at decleration of the hook.
If you want to access the new value of odds after updating it, you would have to either use the source of the updated value (data) if you want to access the value in the same useEffect hook or create another useEffect hook that triggers only when odds has changed:
useEffect(() => {
console.log(odds);
// Do much more
}, [odds]) // <- Tells the hook to run when the variable `odds` has changed.
If you want to see that state has changed in here, you can use
const [odds, setOdds] = useState({})
useEffect(() => {
(async () => {
fetchMock.once('odds', mocks.odds)
let data = await fetch('odds').then(response => response.json())
setOdds(prevData => {
console.log(prevData, data) // {}, {...actual data}
return data
})
})()
}, [])

Resources