How I solve React's functional component and hooks limitation that cause a lot of troubles/bugs
Tags: webdev, react
Published: 2024-05-26 04:30:02

## 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:
- https://www.npmjs.com/package/use-none-reactive-state
- https://www.npmjs.com/package/babel-plugin-none-reactive-effect