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.
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!
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 */ },
}
}
}
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.
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()}`
);
}, []);
Can you guess why this might cause a problem?
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
:
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.
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()}`
);
}, []);
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:
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.
Top comments (0)