DEV Community

Cover image for Optional chaining in the ~real world (React video chat app)
Kimberlee Johnson
Kimberlee Johnson

Posted on

Optional chaining in the ~real world (React video chat app)

I started learning JavaScript in 2019, around the time that optional chaining became a thing.

The optional chaining operator (?.) enables you to read the value of a property located deep within a chain of connected objects without having to check that each reference in the chain is valid. (MDN)

I remember hearing the hearsay about why this was awesome, but, at the time, the above explanation and others and any conversations about a question mark in javascript that isn't the ternary operator still went a bit over my head. Fast forward two years, and I've finally run into optional chaining in the ~real world.

Gif of Real World cast reads I thought I was being punk'd

This post shares that encounter! I'll go over video chat participant "tracks" at the highest level, and then walk through why optional chaining makes sense in this use case.

Before diving in, a quick thank you to my colleague Jess for the lesson that became this post. I'm lucky to learn from her at work everyday and on Twitter too!

Video chat participant tracks

Like a lot of people, I've been on a lot of video calls this year. I also work at Daily, where my colleagues build real-time audio and video APIs. I write documentation for the tools they build and prototype demo apps, so I'm learning a fair amount about the different moving parts behind video and audio-only calls, things I didn't really think about before.

Take, for example, tracks!

Runners on a track

When I join a video call with someone else, I and that other person or people trade audio, video, and sometimes screen media tracks back and forth.

As you've probably experienced, participants' tracks can go through many states. Tracks load as participants join, and then they're playable; they can be muted intentionally or because of a disruption. The Daily API accounts for the following participant track states, for example:

  • blocked
  • off
  • sendable
  • loading
  • playable
  • interrupted

We can find a track's state on the Daily participants object. The object's keys are session id's for each participant, and the corresponding values include lots of details about the participant. For example, here's the participant object for a session_id "e20b7ead-54c3-459e-800a-ca4f21882f2f":

"e20b7ead-54c3-459e-800a-ca4f21882f2f": {
    user_id: "e20b7ead-54c3-459e-800a-ca4f21882f2f",
    audio: true,
    video: false,
    screen: false,
    joined_at: Date(2019-04-30T00:06:32.485Z),
    local: false,
    owner: false,
    session_id: "e20b7ead-54c3-459e-800a-ca4f21882f2f",
    user_name: ""
    tracks: {
      audio: {
        subscribed: boolean,
        state: 'playable',
        blocked?: {
          byDeviceMissing?: boolean,
          byPermissions?: boolean
        },
        off?: {
          byUser?: boolean,
          byBandwidth?: boolean 
        },
        track?: <MediaStreamTrack>
      }
      video: { /* same as above */ },
      screenAudio: { /* same as above */ },
      screenVideo: { /* same as above */ },
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The track's state is deeply nested at participant.tracks.track.state, where track stands for the kind of track (audio, video, screenAudio or screenVideo).

And this is where optional chaining comes in.

Opting into optional chaining

In JavaScript, if an object doesn't exist, trying to access values on that object throws an error.

Mean Girls Lindsay Lohan meme text reads the object does not exist

This can be inconvenient when a value we need is deeply nested, like the participant's video/audio track state. Let's look at an example.

When a participant leaves a call, their audio/video tracks stop. When their audio/video tracks stop, we want to remove their participant tile from the call.

We handle this update the same way we handle all participant updates. I wrote a longer post about how React hooks help us manage state in this video chat app, but tl; dr: the useEffect hook listens for changes to participantUpdated state, and on that change updates the rendered participants list.

participantUpdated stores a string including the name of the event, that participant's session id, and the time the event happened. When a participant's tracks stop, as for other events, we call setParticipantUpdated to change the string. Here's how that looks without optional chaining:

const handleTrackStopped = useCallback((e) => {
  logDailyEvent(e);
  setParticipantUpdated(
`track-stopped-${e.participant.user_id}-${Date.now()}`
  );
}, []);
Enter fullscreen mode Exit fullscreen mode

Can you guess why this might cause a problem?

Schitts Creek Moira Rose gif reads and that's a problem because

Because when a participant leaves a call and their tracks stop, they're no longer a meeting participant. They can't be found on the Daily participants object. .participant does not exist. The console throws an error, Cannot read property 'user_id' of null:

Console error reads TypeError and cannot read property user id of null

From a UI perspective, a black, trackless tile remains even after the participant leaves. This is because setParticipantUpdated can't fire, so the hook listening for the change doesn't update the rendered participant list to remove the absent participant, even though their tracks disappear.

Video chat app with audio muted and empty black tile

Optional chaining helps us avoid this. Let's add the syntax to handleTrackStopped:

const handleTrackStopped = useCallback((e) => {
  logDailyEvent(e);
  setParticipantUpdated(
`track-stopped-${e?.participant?.user_id}-${Date.now()}`
  );
}, []);
Enter fullscreen mode Exit fullscreen mode

Now, those .? evaluate the missing .participant as undefined. If I add a console.log() to handleTrackStopped to see the string passed to state, that's confirmed:

Console reads track-stopped-undefined

With this successful change to participantUpdated state, our hook can register the change, update the participant list, and be sure to remove any trackless tiles.

Remember, it's all optional

Optional chaining makes sense in this demo video chat app for a few reasons. For one thing, our track state data was pretty deeply nested. For another, it's okay if the .participant doesn't exist in our app after they leave (we won't be trying to access their data again once they're gone).

We didn't use optional chaining as our default syntax for every nested object in our app, and it's unlikely that would ever be a good idea. If you're using this syntax in the ~real world, be sure to be explicit about it.

And, if you are using optional chaining, please tell me about it! When have you opted for it recently? Let me know in the comments or over on Twitter.

Discussion (0)