DEV Community

Cover image for Building a single active tab experience
Shubhajit Chatterjee
Shubhajit Chatterjee

Posted on

Building a single active tab experience

I recently had a requirement where, if a user opens the app in multiple browser tabs, only one tab can be active at a time, and the rest are locked.

Core Requirements

Here are the core requirements as I see them:

  1. Uniquely identify each tabs
  2. Store the active tab somewhere
  3. If a user opens a new tab, lock it if there is already an active tab
  4. If the active tab is closed, detect that and inform the other tabs

Sounds simple enough, doesn't it? Take a moment and think about how you'd solve this. It's more interesting than it looks.

Edge Cases

Let’s think through some edge cases:

  1. Next active tab - If the user closes the active tab and multiple tabs are locked, which one should become active?
  2. Stale active tab - If the user closes the browser and returns later, how do we ensure that the previously stored active tab is not considered?
  3. Race condition - If two tabs are opened at the same time, how do you handle the race condition?

Not so simple anymore, right?

Possible Solutions

Let's explore some solutions that I came across.

Local Storage

The most obvious solution is localStorage. Here is how the flow would look like -

  1. Generate a unique identifier for each tab
  2. Store the first tab as active in localStorage
  3. When a new tab is opened, check the storage for any active tab.
  4. If one exists, lock the new tab, otherwise, make it active
  5. Subscribe to storage events to react to active tab changes.
  6. When the active tab is closed, remove it from storage so another tab can become active.

The above does work and new tabs are correctly locked.

Now let's visit the edge cases -

  1. Next active tab - We can store the order in which the tabs are opened, and then based on the order, we can choose to select the next tab. This adds complexity, as we must also remove closed tabs from the order array. We can store this in localStorage as well.
  2. Stale active tab - You might think that we can use beforeunload event and remove it from the localStorage. But the beforeunload event is not that reliable. It won't fire in scenarios like a system crash or an abrupt OS shutdown, leaving stale data in localStorage.
  3. Race condition - Honestly, I couldn't find a clean way to handle this with localStorage alone.

Broadcast Channel

The flow here is almost identical to the localStorage approach, with two differences:

  • We use a BroadcastChannel to communicate directly between tabs
  • Instead of storing state in localStorage, each tab maintains its own in-memory copy of the active tab and the order array, kept in sync via BroadcastChannel messages.

Here is how the flow would be like -

  1. Generate a unique identifier for each tab
  2. When a new tab opens, broadcast a message to ask for the current state
  3. An existing tab responds with the active tab and the order array
  4. Based on the response, decide whether to lock the new tab or make it active
  5. When the active tab is closed, broadcast a message so other tabs can update their in-memory state and elect the next active tab

Now let's revisit the edge cases —

  1. Next active tab - Same as before, we maintain the order array in memory and pick the next one when the active tab closes.
  2. Stale active tab - Interestingly, this is partially better. Since state is in-memory, it dies with the browser — no stale data persists to the next session. However, we still rely on beforeunload to notify other tabs, which is not that reliable.
  3. Race condition - Same problem. Still no clean solution here.

A step forward, but still does not covers everything we need.

Web Locks API

Both approaches we discussed earlier got us close, but we were essentially building a locking mechanism from scratch. What if the browser already had one?

This is where the Web Locks API really shines. Instead of building a locking mechanism from scratch, we can use a browser-provided one that already handles coordination between tabs, including queuing and race conditions.

In our case, the application itself becomes the shared resource. We can define a lock name, say dev:app, and use it to ensure that only one tab can acquire it at a time.

Here is how a typical flow would look like -

  1. When a tab is opened, use the Web Locks API to acquire the lock.
  2. If the lock is acquired, set the current tab to active.

That's all. Everything else is handled by the browser.
Here’s how this looks in React, though the same idea applies across frameworks.

const [active, setActive] = useState(false);

useEffect(() => {
  navigator.locks.request("dev:app", () => {
    setActive(true);
  });
}, []);

if (!active) {
  return <div>This tab is not available</div>;
}

return <div>This tab is available</div>
Enter fullscreen mode Exit fullscreen mode

Let's visit the edge cases for this -

  1. Next active tab - The browser handles assigning the next active tab. When the active tab is closed, the lock is released and the browser automatically assigns it to the next tab in the queue.
  2. Stale active tab - Since the Web Locks API doesn't persist any state, there is no stale data to worry about. When the browser is closed, all locks are released automatically.
  3. Race condition - The browser handles this gracefully. When multiple tabs try to acquire the lock at the same time, the browser ensures only one tab gets it, queuing the rest.

Conclusion

What started as a simple requirement turned into a useful reminder that sometimes the real challenge is not in the implementation, but in knowing what the platform already provides. Instead of building our own coordination logic, we can rely on the Web Locks API to handle it in a much more reliable way.

If you find yourself building a locking mechanism from scratch in the browser, stop and check if the Web Locks API fits. Chances are, it does.

Top comments (0)