-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix useId uniqueness with shared parents + DOM nodes in between #3773
Conversation
Size Change: -196 B (0%) Total Size: 52.9 kB
ℹ️ View Unchanged
|
4092391
to
5400a88
Compare
26b3341
to
8cdab51
Compare
316fb4b
to
d4c2063
Compare
Hi, |
A single counter would not credit async boundaries that could be handled differently during SSR vs CS hydration, the only deterministic similarity should be the shape of the vdom tree after all boundaries resolve |
Right, but the current implementation also depends on mount order: import { useId, useState, useEffect } from 'preact/hooks';
function Id() {
let id = useId();
return <div id={id}>{id}</div>;
}
function Eventually({ children }) {
let [resolved, setResolved] = useState(false);
useEffect(() => {
setTimeout(() => setResolved(true), Math.random() * 1000);
}, []);
return <div>
{resolved && children}
</div>;
}
function Repeat({ children, count }) {
let array = Array.from(Array(count).keys());
return array.map(v => <div key={v}>{children}</div>);
}
export default function App() {
return <div>
<Repeat count={10}>
<Eventually>
<Id />
</Eventually>
</Repeat>
</div>;
} This code assigns different ids on every render. |
I think you're right, we could optimize this further. The main need is to differentiate between counters in async boundaries (=Suspense or Islands) as they could be streamed to the client out of order. It's not ready yet, but I'm working on a streaming variant of |
If you didn't care about the id length (which, sadly, impacts the total size of the prerendered html), you could use this: export function useId() {
const state = getHookState(currentIndex++, 11);
if (!state._value) {
let node = currentComponent._vnode;
console.log(node);
let parent = node._parent;
let path = [currentIndex];
while (parent !== null) {
path.push(node.key ?? parent._children.indexOf(node));
node = parent;
parent = node._parent;
}
state._value = 'P' + path.join('-');
}
return state._value;
} This would make each id a path from the root element to the calling component. (In reverse order, but that hardly matters.) The main problem is that this relies on keys being set correctly. |
8cd98b6
to
8c04d9a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The id concatenation has some issues. (See comment)
ff240e1
to
979c4e6
Compare
This PR changes the id generation for
useId
and fixes an issue where the id wasn't unique anymore when two components were in different HTML nodes, but shared the same component parent.Instead of storing a
_mask
property on every componentvnode
, regardless of whetheruseId
is used or not, we'll only calculate the id whenuseId
is called. When that happens we'll walk up the tree and count the componentvnodes
up until we reach the root. Then we create a component depth array and increase the component depth index of the current component. Joining all indexes will yield the final ID.Whilst this PR drops the need for
vnode._mask
it stores the component depth array on the rootvnode
asvnode._ids
. This behavior is similar to before where ids have the constraint that they are unique per root, not globally unique.Fixes #3772 .