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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@
"$_isSuspended": "__i"
}
}
}
}