In React they have introduced a way to generate unique identifiers no matter what component you are invoking useId from, this is a really good feature for component libraries and it maintains a lot of a11y, … concerns that could disconnect between SSR and hydration. However, creating this kind of unique identifiers for every tree-node is a tricky question.

Theory

When I first started tackling this I came at it from using _depth (this signals the depth of the VNode in the full tree) as well as the position of the child in the parent children. This combination however makes it so that you only have 1 layer of uniqueness, the depth and the correlation to their parent. This falls apart if we look at a tree that has two or more subtrees at its root.

To counteract this problem we would need to keep that identifier stable across all layers of the tree, a so-called prefix tree, this would require us to traverse the tree and assign unique identifiers to every node, in our case we can scope the nodes to only being functional components as those will be the only ones leveraging unique identifiers, we’ll refer to this as “the mask”. Basically creating the following (Excalidraw skills):

Untitled

This gives us 1 unique identifier for every node, if we add a localIdCounter in there we are now set to create ids by means of the following

export const useId = () => {
  const state = getHookState(currentIndex++);
  if(!state._id) state._id = `_P_${vnode.mask}_${currentIndex}`;
  return state._id
}

As we can see the creation of this id isn’t too hard, there are several algo’s we can use to introduce a more brief equivalent of this id, however maintaining all these masks could become quite performance intensive, as essentially this whole concept only matters when we communicate between two disconnected sources (server and client). Essentially after we have hydrated on the client we can start using a globalIdCounter and we should be safe as it’s an ever-incrementing counter.

If we would only be client-side rendering something like this would already be sufficient to get useId to be consistent.

import { useState } from 'preact/hooks';

let counter = 0;
const useId = () => {
  const [id] = useState(() => counter++);
  return id;
}

The problem presents itself with the disconnect between server and client, add deferred boundaries through lazy-components and we face our challenge.

Practice

In practice however creating this mask isn’t quite as easy as described in the above, if we consider the fact that we would be using the Preact options hooks we would soon notice that we would be falling in the following trap

--> Parent (2 children) --> push(0)
--> child-1 --> push(0)
--> we go back up to parent --> pop()
--> child-2 --> how do we know to leverage push(1)

We lack the necessary indicator to know that we are in the second child of the parent, we could use parent._children.indexOf(vnode) however currently we don’t have knowledge about parent or children in the prepass and renderToString passes which means that we are creating a whole set of releases that aren’t compatible with this new concept.

The path of least resistance seems to be adding parent and children pointers to both renderToString and prepass, this way we can tweak our hooks package to do the mask-tracking. The last question mark would be identifying we are in hydration from the hooks package, this to leverage our cheaper approach to generating identifiers.

Resources

https://jukkasuomela.fi/doc/scheduling.pdf

https://softwareengineering.stackexchange.com/questions/143110/inexpensive-generation-of-hierarchical-unique-ids

https://en.wikipedia.org/wiki/Trie