Skip to content

Commit

Permalink
Preact X useId (#3583)
Browse files Browse the repository at this point in the history
* alternative use id

* reliable tagging

* skip fragments

* remove mask var

* reduce diff

* types

* use hash func"

* less bytes

* fix inheritance bug

* optimize

* tweak logic

* fix copy pate

* less bytes

* add compat

* types

* add consideration

* add useId test highlighting subsequent render dangers

* fix client side renders

* revert to prior implementation

* optim
  • Loading branch information
JoviDeCroock committed Sep 3, 2022
1 parent 1427d58 commit 803dbb5
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 2 deletions.
1 change: 1 addition & 0 deletions compat/src/index.d.ts
Expand Up @@ -24,6 +24,7 @@ declare namespace React {
export import useDebugValue = _hooks.useDebugValue;
export import useEffect = _hooks.useEffect;
export import useImperativeHandle = _hooks.useImperativeHandle;
export import useId = _hooks.useId;
export import useLayoutEffect = _hooks.useLayoutEffect;
export import useMemo = _hooks.useMemo;
export import useReducer = _hooks.useReducer;
Expand Down
2 changes: 2 additions & 0 deletions compat/src/index.js
Expand Up @@ -9,6 +9,7 @@ import {
} from 'preact';
import {
useState,
useId,
useReducer,
useEffect,
useLayoutEffect,
Expand Down Expand Up @@ -201,6 +202,7 @@ export {
// React copies the named exports to the default one.
export default {
useState,
useId,
useReducer,
useEffect,
useLayoutEffect,
Expand Down
2 changes: 2 additions & 0 deletions hooks/src/index.d.ts
Expand Up @@ -137,3 +137,5 @@ export function useDebugValue<T>(value: T, formatter?: (value: T) => any): void;
export function useErrorBoundary(
callback?: (error: any, errorInfo: ErrorInfo) => Promise<void> | void
): [any, () => void];

export function useId(): string;
34 changes: 33 additions & 1 deletion hooks/src/index.js
@@ -1,4 +1,4 @@
import { options } from 'preact';
import { Fragment, options } from 'preact';

/** @type {number} */
let currentIndex;
Expand Down Expand Up @@ -27,6 +27,21 @@ const RAF_TIMEOUT = 100;
let prevRaf;

options._diff = vnode => {
if (
typeof vnode.type === 'function' &&
!vnode._mask &&
vnode.type !== Fragment
) {
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 @@ -366,6 +381,23 @@ 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;
}

return state._value;
}
/**
* After paint effects consumer.
*/
Expand Down
7 changes: 6 additions & 1 deletion hooks/src/internal.d.ts
@@ -1,7 +1,8 @@
import {
Component as PreactComponent,
PreactContext,
ErrorInfo
ErrorInfo,
VNode as PreactVNode
} from '../../src/internal';
import { Reducer } from '.';

Expand Down Expand Up @@ -37,6 +38,10 @@ export interface Component extends PreactComponent<any, any> {
__hooks?: ComponentHooks;
}

export interface VNode extends PreactVNode {
_mask?: string;
}

export type HookState =
| EffectHookState
| MemoHookState
Expand Down
132 changes: 132 additions & 0 deletions hooks/test/browser/useId.test.js
@@ -0,0 +1,132 @@
import { createElement, render } from 'preact';
import { useId, useState } from 'preact/hooks';
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';

/** @jsx createElement */

describe('useId', () => {
/** @type {HTMLDivElement} */
let scratch, rerender;

beforeEach(() => {
scratch = setupScratch();
rerender = setupRerender();
});

afterEach(() => {
teardown(scratch);
});

it('keeps the id consistent after an update', () => {
function Comp() {
const id = useId();
return <div id={id} />;
}

render(<Comp />, scratch);
const id = scratch.firstChild.getAttribute('id');
expect(scratch.firstChild.getAttribute('id')).to.equal(id);

render(<Comp />, scratch);
expect(scratch.firstChild.getAttribute('id')).to.equal(id);
});

it('ids are unique according to dom-depth', () => {
function Child() {
const id = useId();
const spanId = useId();
return (
<div id={id}>
<span id={spanId}>h</span>
</div>
);
}

function Comp() {
const id = useId();
return (
<div id={id}>
<Child />
</div>
);
}

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div id="P15361"><span id="P15362">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>'
);
});

it('ids are unique across siblings', () => {
function Child() {
const id = useId();
return <span id={id}>h</span>;
}

function Comp() {
const id = useId();
return (
<div id={id}>
<Child />
<Child />
<Child />
</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>'
);

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>'
);
});

it('correctly handles new elements', () => {
let set;
function Child() {
const id = useId();
return <span id={id}>h</span>;
}

function Stateful() {
const [state, setState] = useState(false);
set = setState;
return (
<div>
<Child />
{state && <Child />}
</div>
);
}

function Comp() {
const id = useId();
return (
<div id={id}>
<Stateful />
</div>
);
}

render(<Comp />, scratch);
expect(scratch.innerHTML).to.equal(
'<div id="P481"><div><span id="P476641">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>'
);
});
});

0 comments on commit 803dbb5

Please sign in to comment.