How I solve React's functional component and hooks limitation that cause a lot of troubles/bugs
Bùi Khôi
Posted on May 26, 2024
I. What's the limitation?
Recently, I took over a project that mostly work on realtime communication(messaging, video call, integrate with different vendors using different sockets). At the moment I joined, there're bunch of bugs that related to glitches stuff.
Take a look at the code below:
const Page = () => {
const [roomName, setRoomName] = useState();
const [numberOfParticipant, setNumberOfParticipant] = useState();
const connection = useVideoCallConnection();
useEffect(() => {
const conference = connection.createConference(roomName);
conference.on('JOINED', () => {
if (numberOfParticipant > 2) {
conference.setViewMode('grid');
}
});
return () => {
conference.off('JOINED');
conference.disconnect();
};
}, [roomName, connection]);
// Component UI
};
It seems fine, but not…
The main issue is: sometime when createConference took a few seconds, someone joined the room in the middle of the creation. numberOfParticipants inside remained the same(numberOfParticipants is number and wasn't included in the dependencies list). It cause the grid layout turned off, even if the participants number larger than two.
You might wonder: "why don't we just add numberOfParticipants to the dep list?"… That's what I thought in the beggining too.
const Page = () => {
const [roomName, setRoomName] = useState();
const [numberOfParticipant, setNumberOfParticipant] = useState();
const connection = useVideoCallConnection();
useEffect(() => {
const conference = connection.createConference(roomName);
conference.on('JOINED', () => {
if (numberOfParticipant > 2) {
conference.setViewMode('grid');
}
});
return () => {
conference.off('JOINED');
conference.disconnect();
};
}, [roomName, connection, numberOfParticipant]);
// Component UI
};
I tried and fixed the problem with grid view but ended up with a new issue: when numberOfParticipant changed, it's disconnect the current conference and re-create a new one...
Because the first solution didn't work, I tried another way by seperating the conference init hook into two useEffect hook:
const Page = () => {
const [roomName, setRoomName] = useState();
const [numberOfParticipant, setNumberOfParticipant] = useState();
const [conference, setConference] = useState();
const connection = useVideoCallConnection();
useEffect(() => {
if (conference) {
conference.on('JOINED', () => {
if (numberOfParticipant > 2) {
conference.setViewMode('grid');
}
});
}
return () => {
if (conference) {
// Call to this method will throw an error, because conference is already off
conference.off('JOINED');
}
}
}, [conference, numberOfParticipant]);
useEffect(() => {
const _conference = connection.createConference(roomName) ;
_conference.connect();
setConference(_conference);
_conference.on('LEAVE', () => {
setConference (null);
});
return () => {
_conference.disconnect();
}
}, [roomName, connection]);
// Component UI
};
By moving setViewMode to another useEffect, we solved the issue(changing the numberOfParticipant only remove the previous listener instance and add a new one)… So the new code is good, isn't it?
Unfortunately, it's not. It worked fine when joining conference but not when leaving.
When the user left the conference(roomName changed). that one was disconnected and set to null. When it was changed, the conferece.off('joined') is called and will throw an execption due to the disconnection happened before.
After a while of trying, I figure it out the limitation here is: If an outside vavariable value is primnitive, I can't get the latest value of it inside useEffect without including it in the dependencies list and re-run the cleanup everytime(react mentioned the same thing here https://react.dev/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects)
II. Why It happened?
useEffect accepts two params
- A function declaration(memorized per depdencies list)
- An array of dependencies list.
Since the function declaration is memorized, the outside variable with priminitive value which is used inside the function will also be memorized per dependencies list. Including a value in the depdencies list make it reactive(changing the variable's value trigger an effect and its cleanup)
III. How I solved that?
Because the rootcause is: variable with priminitive value inside useEffect, we can solve it by duplicate the value with a ref.
const Page = () => {
const [roomName, setRoomName] = useState();
const [numberOfParticipant, setNumberOfParticipant] = useState();
const connection = useVideoCallConnection();
// Create a ref to use inside effect
const numberOfParticipantRef = useRef(numberOfParticipant);
numberOfParticipantRef.current = numberOfParticipant;
useEffect(() => {
const conference = connection.createConference(roomName);
conference.on('JOINED', () => {
// This is reference so it works now!!
if (numberOfParticipantRef.current > 2) {
conference.setViewMode('grid');
}
});
return () => {
conference.off('JOINED');
conference.disconnect();
};
}, [roomName, connection]);
// Component UI
};
numberOfParticipantRef is an object so we could get the latest value inside the effect without re-run anything.
Since my project has a bunch of cases like this and I don't want to do the duplication for all files. I created an npm package hook and a babel plugin to do the job for me
import useNonReactiveState from 'use-none-reactive-state';
const Page = () => {
// Just replace useState with useNonReactiveState and use the babel plugin
const [roomName, setRoomName] = useNonReactiveState();
const [numberOfParticipant, setNumberOfParticipant] = useNonReactiveState();
const connection = useVideoCallConnection();
useEffect(() => {
const conference = connection.createConference(roomName);
conference.on('JOINED', () => {
if (numberOfParticipant > 2) {
conference.setViewMode('grid');
}
});
return () => {
conference.off('JOINED');
conference.disconnect();
};
}, [roomName, connection]);
// Component UI
};
IV. Conclusion
That's how I solved the problem. Please gimme feedbacks If anything is not correct, I really appreciate that. If you feel interest in the package and the plugin, here's the links:
Posted on May 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.