Skip to content
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

Merged
merged 8 commits into from Oct 27, 2022
35 changes: 9 additions & 26 deletions hooks/src/index.js
Expand Up @@ -27,22 +27,6 @@ const RAF_TIMEOUT = 100;
let prevRaf;

options._diff = vnode => {
if (
typeof vnode.type === 'function' &&
marvinhagemeister marked this conversation as resolved.
Show resolved Hide resolved
!vnode._mask &&
// Ignore root Fragment node
vnode._parent !== null
) {
vnode._mask =
(vnode._parent && vnode._parent._mask ? vnode._parent._mask : '') +
(vnode._parent && vnode._parent._children
? vnode._parent._children.indexOf(vnode)
: 0);
} else if (!vnode._mask) {
vnode._mask =
vnode._parent && vnode._parent._mask ? vnode._parent._mask : '';
}

currentComponent = null;
if (oldBeforeDiff) oldBeforeDiff(vnode);
};
Expand Down Expand Up @@ -383,19 +367,18 @@ export function useErrorBoundary(cb) {
];
}

function hash(s) {
let h = 0,
i = s.length;
while (i > 0) {
h = ((h << 5) - h + s.charCodeAt(--i)) | 0;
}
return h;
}

export function useId() {
const state = getHookState(currentIndex++, 11);
if (!state._value) {
state._value = 'P' + hash(currentComponent._vnode._mask) + currentIndex;
// Grab either the root node or the nearest async boundary node.
/** @type {import('./internal.d').VNode} */
let root = currentComponent._vnode;
while (root !== null && !root._mask && root._parent !== null) {
root = root._parent;
}

let mask = root._mask || (root._mask = [0, 0]);
state._value = 'P' + mask[0] + '-' + mask[1]++;
}

return state._value;
Expand Down
2 changes: 1 addition & 1 deletion hooks/src/internal.d.ts
Expand Up @@ -39,7 +39,7 @@ export interface Component extends PreactComponent<any, any> {
}

export interface VNode extends PreactVNode {
_mask?: string;
_mask?: [number, number];
}

export type HookState =
Expand Down
95 changes: 88 additions & 7 deletions hooks/test/browser/useId.test.js
Expand Up @@ -55,12 +55,12 @@ describe('useId', () => {

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div id="P15361"><span id="P15362">h</span></div></div>'
'<div id="P0-0"><div id="P0-1"><span id="P0-2">h</span></div></div>'
);

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div id="P15361"><span id="P15362">h</span></div></div>'
'<div id="P0-0"><div id="P0-1"><span id="P0-2">h</span></div></div>'
);
});

Expand All @@ -83,12 +83,12 @@ describe('useId', () => {

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><span id="P15361">h</span><span id="P15671">h</span><span id="P15981">h</span></div>'
'<div id="P0-0"><span id="P0-1">h</span><span id="P0-2">h</span><span id="P0-3">h</span></div>'
);

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><span id="P15361">h</span><span id="P15671">h</span><span id="P15981">h</span></div>'
'<div id="P0-0"><span id="P0-1">h</span><span id="P0-2">h</span><span id="P0-3">h</span></div>'
);
});

Expand Down Expand Up @@ -121,13 +121,13 @@ describe('useId', () => {

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div><span id="P476641">h</span></div></div>'
'<div id="P0-0"><div><span id="P0-1">h</span></div></div>'
);

set(true);
rerender();
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div><span id="P476641">h</span><span id="P486251">h</span></div></div>'
'<div id="P0-0"><div><span id="P0-1">h</span><span id="P0-2">h</span></div></div>'
);
});

Expand Down Expand Up @@ -332,13 +332,16 @@ describe('useId', () => {
return <Fragment>{children}</Fragment>;
};

const ids = [];
function Foo() {
const id = useId();
ids.push(id);
return <p>{id}</p>;
}

function App() {
const id = useId();
ids.push(id);
return (
<div>
<p>{id}</p>
Expand All @@ -350,6 +353,84 @@ describe('useId', () => {
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<div><p>P481</p><p>P476951</p></div>');
expect(ids[0]).not.to.equal(ids[1]);
});

it('should skip over HTML', () => {
const ids = [];

function Foo() {
const id = useId();
ids.push(id);
return <p>{id}</p>;
}

function App() {
return (
<div>
<span>
<Foo />
</span>
<span>
<Foo />
</span>
</div>
);
}

render(<App />, scratch);
expect(ids[0]).not.to.equal(ids[1]);
});

it('should reset for each renderToString roots', () => {
const ids = [];

function Foo() {
const id = useId();
ids.push(id);
return <p>{id}</p>;
}

function App() {
return (
<div>
<span>
<Foo />
</span>
<span>
<Foo />
</span>
</div>
);
}

const res1 = rts(<App />);
const res2 = rts(<App />);
expect(res1).to.equal(res2);
});

it('should work with conditional components', () => {
function Foo() {
const id = useId();
return <p>{id}</p>;
}
function Bar() {
const id = useId();
return <p>{id}</p>;
}

let update;
function App() {
const [v, setV] = useState(false);
update = setV;
return <div>{!v ? <Foo /> : <Bar />}</div>;
}

render(<App />, scratch);
const first = scratch.innerHTML;

update(v => !v);
rerender();
expect(first).not.to.equal(scratch.innerHTML);
});
});
2 changes: 1 addition & 1 deletion mangle.json
Expand Up @@ -81,4 +81,4 @@
"$_isSuspended": "__i"
}
}
}
}