How React keys identify components

#Editor#React

Why do React keys need to identify components?

In the React Fiber reconciliation process, a key is not just an optimization — it acts as a precise identifier for a component. When keys are duplicated, you'll see a warning in development mode, but the more serious problem is that React ends up reusing components incorrectly, which causes the mount/unmount lifecycle to behave differently from what you intended.

I actually ran into a component duplication issue caused by React keys not being properly distinguished. Through that experience, I came to understand the importance of React keys and how the mount/unmount lifecycle really works.

The problem

In a collaborative editing scenario, an issue came up where cursors were being duplicated whenever three or more people were editing at the same time.

A collaborative editing environment combined with an unprecedented component duplication phenomenon meant I couldn't even guess what was causing the problem. In particular, reproducing a 3+ person collaborative editing situation by myself was tough. By using two computers to open three web pages and running multiple test rounds, I was finally able to pin down the exact issue scenario.

Reproduction scenario:

  • On the screen that User A is observing
  • User B and User C are editing the same piece of text
  • User B moves the cursor to a different text component
  • Result: On User A's screen, User B's cursor shows up in both the previous and the new position at the same time

Identifying the cause

At first I suspected network sync delays in the collaborative editing environment or some issue with state update ordering. After repeated debugging though, I found the cause was surprisingly simple.

It was a React key duplication. On User A's screen, the cursor components of all the remote users were using remoteTextId as their key, and because of this, React couldn't distinguish between the cursors of multiple users editing the same text.

When keys are duplicated during React's reconciliation process, React can't correctly decide which component to keep and which one to remove. So when User B moved to a different text, the cursor at the previous position wasn't unmounted and remained there, while another cursor was added at the new position, resulting in the component duplication phenomenon where a single user's cursor was displayed in two places at once.

Example of a bad key

{remote.map(p => (
  <RemoteCursor
    key={p.remoteTextId} // uses only the text ID — can collide within sibling scope
  />
))}

React reconciliation and the mount/unmount lifecycle

The React Reconciliation process

When a new render happens, React compares the previous Virtual DOM with the new Virtual DOM and decides what changes to apply to the actual DOM. This process is called reconciliation.

Reconciliation tries to perform only the DOM manipulations that are strictly necessary, for the sake of performance. For example, if only 1 item out of a list of 1000 has changed, React updates only that one item instead of redrawing the whole list.

React component lifecycle summary

StageWhenKey actions
MountWhen a component is first created and added to the DOMCreate the instance, add DOM elements, set initial state, register event listeners
UpdateWhen props or state changeCompare old and new values, re-render only the changed parts, reflect changes in the DOM
UnmountWhen the component is removed from the DOMRun cleanup, release event listeners/timers/subscriptions, remove DOM elements and clean up memory

Key points

  • Mount: The "birth" of a component — initialize the necessary resources
  • Update: The "change" of a component — react appropriately to state changes
  • Unmount: The "death" of a component — clean up to prevent memory leaks

The component duplication issue in terms of React reconciliation and the lifecycle

Reconciliation perspective

React identifies components by key when comparing the Virtual DOM. All of the remote users' cursor components were using the same key, and they were editing the same text. As a result, components with the same key were duplicated within a single component tree.

  • React interprets keys as "same key → same component".

  • Because C was continuously editing, the cursor component sharing the same key was alive, and that component was continuously being updated.

  • For this reason, when B moved, B's cursor should normally have been unmounted, but C's update was reflected first, and B's unmount never took effect.

  • In the end, React didn't unmount B's cursor and instead treated it as an update of the component with the same key.

  • At the same time, another B cursor was newly mounted on the new text, so the result was that B's cursor existed in two places simultaneously.

Bug scenario

text1: B moves → (B unmount pending) → C edits → C updates & B unmount skipped & B updated
text2: B mounts

Correct behavior

text1: B moves → B unmounts → C edits → C updates
text2: B mounts

As a result, B's cursor ended up existing on both texts simultaneously — a duplication phenomenon.

Lifecycle perspective

Normally, when the cursor moves, the flow should look like this.

  • Previous block: RemoteCursor(B)unmount (useEffect cleanup)

  • New block: RemoteCursor(B)mount (useEffect)

Example of a normal log

[UNMOUNT] B, alpha
[MOUNT]   B, beta

But in the actual bug, it played out like this.

  • Previous block: RemoteCursor(B) and RemoteCursor(C) share the same key

  • C is still editing → React judges this key as "an already existing component"

  • Instead of unmounting B, it's treated as an update

  • Another B is mounted on the new block

Example of a buggy log

[MOUNT]   B, beta
[UNMOUNT] B, alpha (none)

In short, the problem was that as B moved, the update replaced the call to unmount before it could happen, so the unmount was skipped.

  • A key is not just a value for optimization — it's a component identifier.

  • Especially in cases like collaborative editing where multiple users can edit the same block simultaneously, you absolutely must use a unique value such as sessionId as the key.

  • Otherwise, React can end up mistakenly treating it as an update during reconciliation → skipping the unmount → causing a duplication kind of bug.

OT, sessionId, and the fix

Sessions in OT-based collaboration

Collaborative editing is typically implemented based on the OT (Operational Transformation) algorithm.

  • It transforms users' edit operations so they can be accepted regardless of order or timing.

  • To distinguish between users at this layer, each user is given a unique value called a sessionId.

In other words, at the OT level, "who performed which operation" is identified via the sessionId.

sessionId is needed in the UI too

The cursor display is also UI that shows "who is editing where". But in the existing code, the key was using only remoteTextId without the sessionId. As a result, whenever B and C were in the same block, a key collision occurred, and React couldn't distinguish the cursors properly.

Solved with sessionId

The fix was simple. Incorporate the sessionId — which already exists in OT — into the key design. Using sessionId always guarantees uniqueness within sibling scope.

// Fixed code
{remote.map(p => (
  <RemoteCursor
    key={p.sessionId}
  />
))}

So why does React identify by key...

This issue started off as a simple-looking phenomenon in the UI — cursors that "appeared to be duplicated" — but the root cause lay in the React key design.

At first I suspected network sync issues or state update ordering, but the cause turned out to be surprisingly basic. I'd missed the simple principle of "same key → wrong reconciliation → missing unmount".

A key is not just a means of performance optimization but a mechanism that determines a component's identity. Just because you don't see any warnings doesn't mean you're safe — key collisions can tear down a component's identity in unpredictable ways.