I was trying to make socket connection using React hooks. I am getting a stale value when trying to access a state variable inside socket callback function. users variable inside socket callback is not having the latest value. Using useRef works but it does not rerender the component. What could be the better solution for this ?. Thanks!
const [users, setUsers] = useState([]);
const [socket, setSocket] = useState(null);
const result = useParams();
useEffect(() => {
const socket = io(`${config.host}?meetingId=${result.id}`);
socket.request = promise(socket);
setSocket(socket);
}, []);
useEffect(() => {
if (!socket) return;
let webrtc = null;
socket.on('UserLeft', ({ id }) => {
setUsers(users => users.filter(user => user.id !== id))
});
socket.on('UserAdded', ({ name, id }) => {
const newUser = new OtherUser(name, id);
setUsers(users => [...users, newUser])
})
socket.on('newProducer', async (data) => {
const { socketId, kind } = data;
const consumer = await webrtc.createConsumer(socketId, kind);
// Issue is here, the users array still has stale values
const user = users.find(ele => ele.id === socketId);
// Some more stuff with user
})
}, [socket]);
// Here the output is correct
console.log(users)
return <div>HELLO</div>
You could use useReducer to store mutating variables and add logic into the handler
Related
Does the following code create duplicate on.message listeners ?
when i execute on.message once, the payloads array takes it's initial value each time
export default function ProjectChat() {
const { id: projectId } = useParams()
const [payloads, setPayloads] = useState([])
const [messageBody, setMessageBody] = useState('')
const [webSocket, setWebSocket] = useState(null)
// Create the connection
useEffect(() => {
const url = `ws://localhost:3000/ws/${projectId}`;
const ws = new WebSocket(url);
setWebSocket(ws)
return () => ws.close()
}, [])
useEffect(() => {
if (webSocket !== null) {
webSocket.onmessage = e => {
const payload = JSON.parse(e.data);
const allMessages = [...payloads, payload]
setPayloads(allMessages)
}
}
}, [webSocket, payloads])
return <p>jsx</p>
}
You can't create duplicate listeners with on.message. Every time you define on.message, you're over-writing the previous listener.
The addListener is the method which can lead to multiple listeners. In that case, you would want to run removeEventListener() on the listener you created inside your component's unmount phase return () => removeEvenListener(...).
I am having a bit of trouble setting this up.
I have a folder that deals with all the Db API, so that concerns are separated.
I have one function that opens a connection and gets realtime updates whenever a value in the Db changes (Firebase Firestore).
I call this "listener" function once and would like to keep receiving the real time values within the function that invokes the "listener" function.
Any ideas how I could achieve this?
This is my code:
// LISTENER FN
export const getConnectionRequestsFromDb = () => {
const uid = getUID()
const q = query(
collection(database, "inbox"),
where("uids", "array-contains-any", [uid]),
where("type", "==", "connectionRequest"),
limit(50)
)
const data = []
const unsubscribe = onSnapshot(q, (querySnapshot) => {
// Initially return an empty array, milliseconds later the actual values
querySnapshot.forEach((doc) => data.push(doc.data()))
})
const formattedData = convertDatesIntoJsTimestamps(data)
return [formattedData, null, unsubscribe]
}
// INVOKING FN
export const getConnectionRequests = () => {
return async (dispatch) => {
dispatch({ type: "CONNECTIONS_ACTIONS/GET_CONNECTIONS_REQUEST/pending" })
// I want to keep listening for realtime updates here and dispatch payloads accordingly
const [data, error, unsubscribe] = getConnectionRequestsFromDb()
if (data) {
return dispatch({
type: "CONNECTIONS_ACTIONS/GET_CONNECTIONS_REQUEST/fulfilled",
payload: data,
})
}
}
}
this piece of code works when I press save in visual studio code. But If I refresh the preview page in the browser it shows me this error: Unhandled Rejection (FirebaseError): Function Query.where() called with invalid data. Unsupported field value: undefined
let { id } = useParams();
const [video, setVideo] = React.useState([]);
const [show, setShow] = React.useState([]);
const classes = useStyles();
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore();
const data = await db
.collection("videos")
.where('path', '==', id)
.get()
setVideo(data.docs.map(doc => doc.data()));
}
fetchData()
}, [])
let showUrl = video.map(video =>(video.uploadBy));
console.log(showUrl[0]);
let videoDate = video.map(video =>(video.date.toDate()));
console.log(videoDate[0]);
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore();
const data = await db
.collection("shows")
.where('urlPath', '==', showUrl[0])
.get()
setShow(data.docs.map(doc => doc.data()));
}
fetchData()
}, [])
I think that the problem is that I'm trying to declare the variable "showUrl" in the wrong way. The console.log(showUrl[0]) works perfectly. It prints exactly the value that I need.
Both these useEffect calls fire as soon as the component mounts. If you are getting the id for your first useEffect from url parameters or such, it's probably there immediately and the call Firestore query should work.
However, when your second useEffect fires, the state 'video' is still set to an empty array. Therefore the showUrl variable is also an empty array, and showUrl[0] is undefined.
What you could do for your second useEffect is this:
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore();
const data = await db
.collection("shows")
.where('urlPath', '==', showUrl[0])
.get()
setShow(data.docs.map(doc => doc.data()));
}
video.length && fetchData()
}, [video])
So you are only calling the fetchData() function if the array in 'video' state has more than 0 items, and you add it to the useEffect dependency array, so the useEffect gets ran every time 'video' changes.
const [data, setData] = useState([])
const getDataFromFirebase = async () => {
let response = await firestore.collection('someDatabase').get()
response.forEach(item => setData([...data, item.data().name]))
}
useEffect(() => {
getDataFromFirebase()
},[])
data is being overridden with the latest value instead of adding all the values to the array.
The reason is time taken to add item is very less thats why before reflecting, it got override. You have to use prevState in setData . Try this:
const [data, setData] = useState([])
const getDataFromFirebase = async () => {
let response = await firestore.collection('someDatabase').get()
response.forEach(item => setData(prevState => ([
...prevState, item.data().name])
);
}
useEffect(() => {
getDataFromFirebase()
},[])
Use the callback in setData
setData(prevState => ([
...prevState, item.data().name
]));
let response = await firestore.collection('someDatabase').get()
response.forEach(item => setData([...data, item.data().name]))
I'm not familiar with firestore, but that promise will be resolved once, and you should do something like this instead:
const dataToAdd = response.map(item => item.data().name)
setData(prevState => ([...prevState, ...dataToAdd])
You are rerending component each time the setData is being called and you shouldn't do it in a synced loop.
prevState is necessary here because you are working in an asynchronous function. In theory, it should work without it after using a solution with dataToAdd if you don't change the state anywhere else.
try this fire setState once but build the array before :
const [data, setData] = useState([])
const getDataFromFirebase = async () => {
let response = await firestore.collection('someDatabase').get()
const d = response.map(item=> item.data().name)
setData(d)
}
useEffect(() => {
getDataFromFirebase()
},[])
firing setData multiple times will cause multiple rerenders so here it's fire once.
In your code below the value of data will be always [] even if you change the data later.
const getDataFromFirebase = async () => {
let response = await firestore.collection('someDatabase').get()
response.forEach(item => setData([...data, item.data().name]))
}
This is what docs say about it
Mutations, subscriptions, timers, logging, and other side effects are
not allowed inside the main body of a function component (referred to
as React’s render phase). Doing so will lead to confusing bugs and
inconsistencies in the UI.
Its not a good idea to call setData in each loop. Populate an array and pass it to setData once loop is complete.
const getDataFromFirebase = async () => {
let response = await firestore.collection('someDatabase').get();
let newData = [];
response.forEach(item => newData.push(item.data().name));
// now set the new data
setData(prevData=> ([...prevData, ...newData]));
// or you can use Array.concat
// setData(prevData=> (prevData.concat(newData)));
}
I have a functional component with Hooks:
const Filters = () => {
const [options, setOptions] = useState([]);
const getOption = type => options.find(el => el.name === type);
useEffect(() => {
fetch('someURL')
.then(resp => resp.json())
.then(result => setOptions(result.data), error => console.log(error));
// trying to check options array now
console.log(getOption('type')); // returns undefined
}, []);
}
The purpose of this approach is to fetch a data, then run this data through a computed function, to get a single object based on getOption(type). If i use useEffect(() => fetch(), [options]); then i'll get endless loop with console.log() outputs.
So, setOptions(result.data) is async i guess, just like setState in a class component, but doesn't accept a second parameter to use when async request is done.
I want to modify my options array after a fetch() is done with my getOption() function.
You could use another useEffect to execute a function when options is modified:
const Filters = () => {
const [options, setOptions] = useState([]);
const getOption = type => options.find(el => el.name === type);
useEffect(() => {
fetch('someURL')
.then(resp => resp.json())
.then(result => setOptions(result.data), error => console.log(error));
}, []);
useEffect(() => {
if (!options) {
return;
}
console.log(getOption('type'));
}, [options])
}