From a6939541b9147051ba34cb12f9845ba39c50a317 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 10 Nov 2024 13:32:53 +0100 Subject: [PATCH 01/38] Remove replaceNode --- src/internal.d.ts | 2 +- src/render.js | 27 ++-- test/browser/replaceNode.test.js | 239 ------------------------------- v17.md | 18 +++ 4 files changed, 28 insertions(+), 258 deletions(-) delete mode 100644 test/browser/replaceNode.test.js create mode 100644 v17.md diff --git a/src/internal.d.ts b/src/internal.d.ts index 85e704be65..7a78d668a4 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -95,6 +95,7 @@ export interface PreactElement extends preact.ContainerNode { data?: CharacterData['data']; // Property to set __dangerouslySetInnerHTML innerHTML?: Element['innerHTML']; + remove?: Element['remove']; // Attribute reading and setting readonly attributes?: Element['attributes']; @@ -162,7 +163,6 @@ export interface Component

extends Omit, // When component is functional component, this is reset to functional component constructor: ComponentType

; state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks - base?: PreactElement; _dirty: boolean; _force?: boolean; diff --git a/src/render.js b/src/render.js index 5080853a47..e1c60b6ba5 100644 --- a/src/render.js +++ b/src/render.js @@ -22,19 +22,16 @@ export function render(vnode, parentDom, replaceNode) { // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in // hydration mode or not by passing the `hydrate` function instead of a DOM // element.. - let isHydrating = typeof replaceNode == 'function'; + let isHydrating = replaceNode === hydrate; // To be able to support calling `render()` multiple times on the same // DOM node, we need to obtain a reference to the previous tree. We do // this by assigning a new `_children` property to DOM nodes which points // to the last rendered tree. By default this property is not present, which // means that we are mounting a new tree for the first time. - let oldVNode = isHydrating - ? NULL - : (replaceNode && replaceNode._children) || parentDom._children; + let oldVNode = isHydrating ? NULL : parentDom._children; - vnode = ((!isHydrating && replaceNode) || parentDom)._children = - createElement(Fragment, NULL, [vnode]); + vnode = parentDom._children = createElement(Fragment, NULL, [vnode]); // List of effects that need to be called after diffing. let commitQueue = [], @@ -47,19 +44,13 @@ export function render(vnode, parentDom, replaceNode) { oldVNode || EMPTY_OBJ, EMPTY_OBJ, parentDom.namespaceURI, - !isHydrating && replaceNode - ? [replaceNode] - : oldVNode - ? NULL - : parentDom.firstChild - ? slice.call(parentDom.childNodes) - : NULL, + oldVNode + ? NULL + : parentDom.firstChild + ? slice.call(parentDom.childNodes) + : NULL, commitQueue, - !isHydrating && replaceNode - ? replaceNode - : oldVNode - ? oldVNode._dom - : parentDom.firstChild, + oldVNode ? oldVNode._dom : parentDom.firstChild, isHydrating, refQueue ); diff --git a/test/browser/replaceNode.test.js b/test/browser/replaceNode.test.js deleted file mode 100644 index 63cdd2d65e..0000000000 --- a/test/browser/replaceNode.test.js +++ /dev/null @@ -1,239 +0,0 @@ -import { createElement, render, Component } from 'preact'; -import { - setupScratch, - teardown, - serializeHtml, - sortAttributes -} from '../_util/helpers'; - -/** @jsx createElement */ - -describe('replaceNode parameter in render()', () => { - let scratch; - - /** - * @param {HTMLDivElement} container - * @returns {HTMLDivElement[]} - */ - function setupABCDom(container) { - return ['a', 'b', 'c'].map(id => { - const child = document.createElement('div'); - child.id = id; - container.appendChild(child); - - return child; - }); - } - - beforeEach(() => { - scratch = setupScratch(); - }); - - afterEach(() => { - teardown(scratch); - }); - - it('should use replaceNode as render root and not inject into it', () => { - setupABCDom(scratch); - const childA = scratch.querySelector('#a'); - - render(

contents
, scratch, childA); - expect(scratch.querySelector('#a')).to.equalNode(childA); - expect(childA.innerHTML).to.equal('contents'); - }); - - it('should not remove siblings of replaceNode', () => { - setupABCDom(scratch); - const childA = scratch.querySelector('#a'); - - render(
, scratch, childA); - expect(scratch.innerHTML).to.equal( - '
' - ); - }); - - it('should notice prop changes on replaceNode', () => { - setupABCDom(scratch); - const childA = scratch.querySelector('#a'); - - render(
, scratch, childA); - expect(sortAttributes(String(scratch.innerHTML))).to.equal( - sortAttributes( - '
' - ) - ); - }); - - it('should unmount existing components', () => { - const unmount = sinon.spy(); - const mount = sinon.spy(); - class App extends Component { - componentDidMount() { - mount(); - } - - componentWillUnmount() { - unmount(); - } - - render() { - return
App
; - } - } - - render( -
- -
, - scratch - ); - expect(scratch.innerHTML).to.equal('
App
'); - expect(mount).to.be.calledOnce; - - render(
new
, scratch, scratch.querySelector('#a')); - expect(scratch.innerHTML).to.equal('
new
'); - expect(unmount).to.be.calledOnce; - }); - - it('should unmount existing components in prerendered HTML', () => { - const unmount = sinon.spy(); - const mount = sinon.spy(); - class App extends Component { - componentDidMount() { - mount(); - } - - componentWillUnmount() { - unmount(); - } - - render() { - return App; - } - } - - scratch.innerHTML = `
`; - - const childContainer = scratch.querySelector('#child'); - - render(, childContainer); - expect(serializeHtml(childContainer)).to.equal('App'); - expect(mount).to.be.calledOnce; - - render(
, scratch, scratch.firstElementChild); - expect(serializeHtml(scratch)).to.equal('
'); - expect(unmount).to.be.calledOnce; - }); - - it('should render multiple render roots in one parentDom', () => { - setupABCDom(scratch); - const childA = scratch.querySelector('#a'); - const childB = scratch.querySelector('#b'); - const childC = scratch.querySelector('#c'); - - const expectedA = '
childA
'; - const expectedB = '
childB
'; - const expectedC = '
childC
'; - - render(
childA
, scratch, childA); - render(
childB
, scratch, childB); - render(
childC
, scratch, childC); - - expect(scratch.innerHTML).to.equal(`${expectedA}${expectedB}${expectedC}`); - }); - - it('should call unmount when working with replaceNode', () => { - const mountSpy = sinon.spy(); - const unmountSpy = sinon.spy(); - class MyComponent extends Component { - componentDidMount() { - mountSpy(); - } - componentWillUnmount() { - unmountSpy(); - } - render() { - return
My Component
; - } - } - - const container = document.createElement('div'); - scratch.appendChild(container); - - render(, scratch, container); - expect(mountSpy).to.be.calledOnce; - - render(
Not my component
, document.body, container); - expect(unmountSpy).to.be.calledOnce; - }); - - it('should double replace', () => { - const container = document.createElement('div'); - scratch.appendChild(container); - - render(
Hello
, scratch, scratch.firstElementChild); - expect(scratch.innerHTML).to.equal('
Hello
'); - - render(
Hello
, scratch, scratch.firstElementChild); - expect(scratch.innerHTML).to.equal('
Hello
'); - }); - - it('should replaceNode after rendering', () => { - function App({ i }) { - return

{i}

; - } - - render(, scratch); - expect(scratch.innerHTML).to.equal('

2

'); - - render(, scratch, scratch.firstChild); - expect(scratch.innerHTML).to.equal('

3

'); - }); - - it("shouldn't remove elements on subsequent renders with replaceNode", () => { - const placeholder = document.createElement('div'); - scratch.appendChild(placeholder); - const App = () => ( -
- New content - -
- ); - - render(, scratch, placeholder); - expect(scratch.innerHTML).to.equal( - '
New content
' - ); - - render(, scratch, placeholder); - expect(scratch.innerHTML).to.equal( - '
New content
' - ); - }); - - it('should remove redundant elements on subsequent renders with replaceNode', () => { - const placeholder = document.createElement('div'); - scratch.appendChild(placeholder); - const App = () => ( -
- New content - -
- ); - - render(, scratch, placeholder); - expect(scratch.innerHTML).to.equal( - '
New content
' - ); - - placeholder.appendChild(document.createElement('span')); - expect(scratch.innerHTML).to.equal( - '
New content
' - ); - - render(, scratch, placeholder); - expect(scratch.innerHTML).to.equal( - '
New content
' - ); - }); -}); diff --git a/v17.md b/v17.md new file mode 100644 index 0000000000..0247d93069 --- /dev/null +++ b/v17.md @@ -0,0 +1,18 @@ +# Breaking changes + +- The package now only exports ESM https://github.com/graphql/graphql-js/pull/3552 +- `GraphQLError` can now only be constructed with a message and options rather than also with positional arguments https://github.com/graphql/graphql-js/pull/3577 +- `createSourceEventStream` can now only be used with with an object-argument rather than alsow with positional arguments https://github.com/graphql/graphql-js/pull/3635 +- Allow `subscribe` to return a value rather than only a Promise, this makes the returned type in line with `execute` https://github.com/graphql/graphql-js/pull/3620 +- `execute` throws an error when it sees a `@defer` or `@stream` directive, use `experimentalExecuteIncrementally` instead https://github.com/graphql/graphql-js/pull/3722 +- Remove support for defer/stream from subscriptions, in case you have fragments that you use with `defer/stream` that end up in a subscription, use the `if` argument of the directive to disable it in your subscriptin operations https://github.com/graphql/graphql-js/pull/3742 + +## Removals + +- Remove `graphql/subscription` module https://github.com/graphql/graphql-js/pull/3570 +- Remove `getOperationType` function https://github.com/graphql/graphql-js/pull/3571 +- Remove `getVisitFn` function https://github.com/graphql/graphql-js/pull/3580 +- Remove `printError` and `formatError` utils https://github.com/graphql/graphql-js/pull/3582 +- Remove `assertValidName` and `isValidNameError` utils https://github.com/graphql/graphql-js/pull/3572 +- Remove `assertValidExecutionArguments` function https://github.com/graphql/graphql-js/pull/3643 +- Remove `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types, use `Kind`, `TokenKind` and `DirectiveLocation` instead. https://github.com/graphql/graphql-js/pull/3579 From 610e8185c79af2ed54ac5aaf9a09c1e871f27656 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 10 Nov 2024 13:35:09 +0100 Subject: [PATCH 02/38] Leverage Object.assign --- compat/src/util.js | 13 ++----------- debug/src/util.js | 12 +----------- src/diff/index.js | 2 +- src/util.js | 17 ++--------------- 4 files changed, 6 insertions(+), 38 deletions(-) diff --git a/compat/src/util.js b/compat/src/util.js index 8ec376942b..23d73fd916 100644 --- a/compat/src/util.js +++ b/compat/src/util.js @@ -1,14 +1,4 @@ -/** - * Assign properties from `props` to `obj` - * @template O, P The obj and props types - * @param {O} obj The object to copy properties to - * @param {P} props The object to copy properties from - * @returns {O & P} - */ -export function assign(obj, props) { - for (let i in props) obj[i] = props[i]; - return /** @type {O & P} */ (obj); -} +export const assign = Object.assign; /** * Check if two objects have a different shape @@ -29,5 +19,6 @@ export function shallowDiffers(a, b) { * @returns {boolean} */ export function is(x, y) { + // TODO: can we replace this with Object.is? return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); } diff --git a/debug/src/util.js b/debug/src/util.js index be4228b9b6..2dddcea736 100644 --- a/debug/src/util.js +++ b/debug/src/util.js @@ -1,14 +1,4 @@ -/** - * Assign properties from `props` to `obj` - * @template O, P The obj and props types - * @param {O} obj The object to copy properties to - * @param {P} props The object to copy properties from - * @returns {O & P} - */ -export function assign(obj, props) { - for (let i in props) obj[i] = props[i]; - return /** @type {O & P} */ (obj); -} +export const assign = Object.assign; export function isNaN(value) { return value !== value; diff --git a/src/diff/index.js b/src/diff/index.js index 5603586ae6..2b97b7120a 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -249,7 +249,7 @@ export function diff( c.state = c._nextState; if (c.getChildContext != NULL) { - globalContext = assign(assign({}, globalContext), c.getChildContext()); + globalContext = assign({}, globalContext, c.getChildContext()); } if (isClassComponent && !isNew && c.getSnapshotBeforeUpdate != NULL) { diff --git a/src/util.js b/src/util.js index 647519175f..4b183368c1 100644 --- a/src/util.js +++ b/src/util.js @@ -1,19 +1,8 @@ import { EMPTY_ARR } from './constants'; export const isArray = Array.isArray; - -/** - * Assign properties from `props` to `obj` - * @template O, P The obj and props types - * @param {O} obj The object to copy properties to - * @param {P} props The object to copy properties from - * @returns {O & P} - */ -export function assign(obj, props) { - // @ts-expect-error We change the type of `obj` to be `O & P` - for (let i in props) obj[i] = props[i]; - return /** @type {O & P} */ (obj); -} +export const assign = Object.assign; +export const slice = EMPTY_ARR.slice; /** * Remove a child node from its parent if attached. This is a workaround for @@ -24,5 +13,3 @@ export function assign(obj, props) { export function removeNode(node) { if (node && node.parentNode) node.parentNode.removeChild(node); } - -export const slice = EMPTY_ARR.slice; From 90e2902f8535be554ebf3ba1209ee131063706e9 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 10 Nov 2024 13:40:24 +0100 Subject: [PATCH 03/38] Remove IE11 unmount hack --- src/diff/index.js | 8 ++++---- src/util.js | 10 ---------- test/_util/logCall.js | 4 ---- test/browser/fragments.test.js | 9 ++++++--- test/browser/hydrate.test.js | 9 ++++++--- test/browser/keys.test.js | 3 +++ test/browser/placeholders.test.js | 6 +++--- test/browser/render.test.js | 6 +++--- 8 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index 2b97b7120a..6cfe0260b4 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -13,7 +13,7 @@ import { BaseComponent, getDomSibling } from '../component'; import { Fragment } from '../create-element'; import { diffChildren } from './children'; import { setProperty } from './props'; -import { assign, isArray, removeNode, slice } from '../util'; +import { assign, isArray, slice } from '../util'; import options from '../options'; /** @@ -564,7 +564,7 @@ function diffElementNodes( // Remove children that are not part of any vnode. if (excessDomChildren != NULL) { for (i = excessDomChildren.length; i--; ) { - removeNode(excessDomChildren[i]); + if (excessDomChildren[i]) excessDomChildren[i].remove(); } } } @@ -668,8 +668,8 @@ export function unmount(vnode, parentVNode, skipRemove) { } } - if (!skipRemove) { - removeNode(vnode._dom); + if (!skipRemove && vnode._dom != null && typeof vnode.type != 'function') { + vnode._dom.remove(); } vnode._component = vnode._parent = vnode._dom = UNDEFINED; diff --git a/src/util.js b/src/util.js index 4b183368c1..8b112f69a2 100644 --- a/src/util.js +++ b/src/util.js @@ -3,13 +3,3 @@ import { EMPTY_ARR } from './constants'; export const isArray = Array.isArray; export const assign = Object.assign; export const slice = EMPTY_ARR.slice; - -/** - * Remove a child node from its parent if attached. This is a workaround for - * IE11 which doesn't support `Element.prototype.remove()`. Using this function - * is smaller than including a dedicated polyfill. - * @param {import('./index').ContainerNode} node The node to remove - */ -export function removeNode(node) { - if (node && node.parentNode) node.parentNode.removeChild(node); -} diff --git a/test/_util/logCall.js b/test/_util/logCall.js index de27036401..3fb094ca78 100644 --- a/test/_util/logCall.js +++ b/test/_util/logCall.js @@ -31,10 +31,6 @@ export function logCall(obj, method) { let operation; switch (method) { - case 'removeChild': { - operation = `${serialize(c)}.remove()`; - break; - } case 'insertBefore': { if (args[1] === null && args.length === 2) { operation = `${serialize(this)}.appendChild(${serialize(args[0])})`; diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index efd964d9b7..b1f4f1b9fc 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -37,12 +37,14 @@ describe('Fragment', () => { let resetInsertBefore; let resetAppendChild; - let resetRemoveChild; + let resetRemove; + let resetRemoveText; before(() => { resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetAppendChild = logCall(Element.prototype, 'appendChild'); - resetRemoveChild = logCall(Element.prototype, 'removeChild'); + resetRemove = logCall(Element.prototype, 'remove'); + resetRemoveText = logCall(Text.prototype, 'remove'); // logCall(CharacterData.prototype, 'remove'); // TODO: Consider logging setting set data // ``` @@ -58,7 +60,8 @@ describe('Fragment', () => { after(() => { resetInsertBefore(); resetAppendChild(); - resetRemoveChild(); + resetRemove(); + resetRemoveText(); }); beforeEach(() => { diff --git a/test/browser/hydrate.test.js b/test/browser/hydrate.test.js index 0d49b94085..d97e2e0655 100644 --- a/test/browser/hydrate.test.js +++ b/test/browser/hydrate.test.js @@ -25,8 +25,9 @@ describe('hydrate()', () => { let resetAppendChild; let resetInsertBefore; - let resetRemoveChild; let resetRemove; + let resetRemoveText; + let resetRemoveComment; let resetSetAttribute; let resetRemoveAttribute; let rerender; @@ -34,8 +35,9 @@ describe('hydrate()', () => { before(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); - resetRemoveChild = logCall(Element.prototype, 'removeChild'); resetRemove = logCall(Element.prototype, 'remove'); + resetRemoveComment = logCall(Comment.prototype, 'remove'); + resetRemoveText = logCall(Text.prototype, 'remove'); resetSetAttribute = logCall(Element.prototype, 'setAttribute'); resetRemoveAttribute = logCall(Element.prototype, 'removeAttribute'); }); @@ -43,10 +45,11 @@ describe('hydrate()', () => { after(() => { resetAppendChild(); resetInsertBefore(); - resetRemoveChild(); resetRemove(); + resetRemoveText(); resetSetAttribute(); resetRemoveAttribute(); + resetRemoveComment(); }); beforeEach(() => { diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 73c8911c20..5b11afbaa1 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -57,12 +57,14 @@ describe('keys', () => { let resetInsertBefore; let resetRemoveChild; let resetRemove; + let resetRemoveText; before(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'removeChild'); resetRemove = logCall(Element.prototype, 'remove'); + resetRemoveText = logCall(Text.prototype, 'remove'); }); after(() => { @@ -70,6 +72,7 @@ describe('keys', () => { resetInsertBefore(); resetRemoveChild(); resetRemove(); + resetRemoveText(); }); beforeEach(() => { diff --git a/test/browser/placeholders.test.js b/test/browser/placeholders.test.js index 0fbdb6f181..a5c7bcb7b0 100644 --- a/test/browser/placeholders.test.js +++ b/test/browser/placeholders.test.js @@ -56,20 +56,20 @@ describe('null placeholders', () => { let resetAppendChild; let resetInsertBefore; - let resetRemoveChild; + let resetRemoveText; let resetRemove; before(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); - resetRemoveChild = logCall(Element.prototype, 'removeChild'); resetRemove = logCall(Element.prototype, 'remove'); + resetRemoveText = logCall(Text.prototype, 'remove'); }); after(() => { resetAppendChild(); resetInsertBefore(); - resetRemoveChild(); + resetRemoveText(); resetRemove(); }); diff --git a/test/browser/render.test.js b/test/browser/render.test.js index f4f0f4ed86..65c94e3b50 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -31,7 +31,7 @@ describe('render()', () => { let resetAppendChild; let resetInsertBefore; - let resetRemoveChild; + let resetRemoveText; let resetRemove; beforeEach(() => { @@ -46,14 +46,14 @@ describe('render()', () => { before(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); - resetRemoveChild = logCall(Element.prototype, 'removeChild'); + resetRemoveText = logCall(Text.prototype, 'remove'); resetRemove = logCall(Element.prototype, 'remove'); }); after(() => { resetAppendChild(); resetInsertBefore(); - resetRemoveChild(); + resetRemoveText(); resetRemove(); }); From 826b5a3de12c5db9b79dc27646aead3a94122c47 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 10 Nov 2024 13:42:49 +0100 Subject: [PATCH 04/38] Remove select IE11 fix --- TODO.md | 4 ++++ src/diff/index.js | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000000..c1b41d8f40 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +- https://github.com/preactjs/preact/pull/4362 +- https://github.com/preactjs/preact/pull/4358 +- https://github.com/preactjs/preact/pull/4361 +- https://github.com/preactjs/preact/pull/4460 diff --git a/src/diff/index.js b/src/diff/index.js index 6cfe0260b4..12a052ab19 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -580,12 +580,7 @@ function diffElementNodes( // despite the attribute not being present. When the attribute // is missing the progress bar is treated as indeterminate. // To fix that we'll always update it when it is 0 for progress elements - (inputValue !== dom[i] || - (nodeType == 'progress' && !inputValue) || - // This is only for IE 11 to fix null} />, scratch); - expect(window.Symbol).to.have.been.calledOnce; - expect(proto.addEventListener).to.have.been.calledOnce; - expect(proto.addEventListener).to.have.been.calledWithExactly( - eventType, - sinon.match.func, - false - ); - - window.Symbol.restore(); - if (isIE11) { - window.Symbol = undefined; - } - }); - it('should support onAnimationEnd', () => { const func = sinon.spy(() => {}); render(
, scratch); diff --git a/compat/test/browser/exports.test.js b/compat/test/browser/exports.test.js index cced23da45..d96e4bed4c 100644 --- a/compat/test/browser/exports.test.js +++ b/compat/test/browser/exports.test.js @@ -58,7 +58,6 @@ describe('compat exports', () => { expect(Compat.Children.toArray).to.exist.and.be.a('function'); expect(Compat.Children.only).to.exist.and.be.a('function'); expect(Compat.unmountComponentAtNode).to.exist.and.be.a('function'); - expect(Compat.unstable_batchedUpdates).to.exist.and.be.a('function'); expect(Compat.version).to.exist.and.be.a('string'); expect(Compat.startTransition).to.be.a('function'); }); @@ -99,7 +98,6 @@ describe('compat exports', () => { expect(Named.Children.toArray).to.exist.and.be.a('function'); expect(Named.Children.only).to.exist.and.be.a('function'); expect(Named.unmountComponentAtNode).to.exist.and.be.a('function'); - expect(Named.unstable_batchedUpdates).to.exist.and.be.a('function'); expect(Named.version).to.exist.and.be.a('string'); }); }); diff --git a/compat/test/browser/forwardRef.test.js b/compat/test/browser/forwardRef.test.js index f69d5ae014..d9fe00e600 100644 --- a/compat/test/browser/forwardRef.test.js +++ b/compat/test/browser/forwardRef.test.js @@ -35,7 +35,7 @@ describe('forwardRef', () => { expect(App.prototype.isReactComponent).to.equal(true); }); - it('should have $$typeof property', () => { + it.skip('should have $$typeof property', () => { let App = forwardRef((_, ref) =>
foo
); const expected = getSymbol('react.forward_ref', 0xf47); expect(App.$$typeof).to.equal(expected); diff --git a/src/diff/index.js b/src/diff/index.js index 28f0158904..57fa47ab00 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -305,7 +305,9 @@ export function diff( newVNode._dom = oldDom; } else { for (let i = excessDomChildren.length; i--; ) { - removeNode(excessDomChildren[i]); + if (excessDomChildren[i]) { + excessDomChildren[i].remove(); + } } } } else { diff --git a/v17.md b/v17.md deleted file mode 100644 index 0247d93069..0000000000 --- a/v17.md +++ /dev/null @@ -1,18 +0,0 @@ -# Breaking changes - -- The package now only exports ESM https://github.com/graphql/graphql-js/pull/3552 -- `GraphQLError` can now only be constructed with a message and options rather than also with positional arguments https://github.com/graphql/graphql-js/pull/3577 -- `createSourceEventStream` can now only be used with with an object-argument rather than alsow with positional arguments https://github.com/graphql/graphql-js/pull/3635 -- Allow `subscribe` to return a value rather than only a Promise, this makes the returned type in line with `execute` https://github.com/graphql/graphql-js/pull/3620 -- `execute` throws an error when it sees a `@defer` or `@stream` directive, use `experimentalExecuteIncrementally` instead https://github.com/graphql/graphql-js/pull/3722 -- Remove support for defer/stream from subscriptions, in case you have fragments that you use with `defer/stream` that end up in a subscription, use the `if` argument of the directive to disable it in your subscriptin operations https://github.com/graphql/graphql-js/pull/3742 - -## Removals - -- Remove `graphql/subscription` module https://github.com/graphql/graphql-js/pull/3570 -- Remove `getOperationType` function https://github.com/graphql/graphql-js/pull/3571 -- Remove `getVisitFn` function https://github.com/graphql/graphql-js/pull/3580 -- Remove `printError` and `formatError` utils https://github.com/graphql/graphql-js/pull/3582 -- Remove `assertValidName` and `isValidNameError` utils https://github.com/graphql/graphql-js/pull/3572 -- Remove `assertValidExecutionArguments` function https://github.com/graphql/graphql-js/pull/3643 -- Remove `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types, use `Kind`, `TokenKind` and `DirectiveLocation` instead. https://github.com/graphql/graphql-js/pull/3579 From 1a2beff405e1b78adbd99d538856d8091f70c888 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:14:03 -0600 Subject: [PATCH 10/38] test: Drop unused/broken test (#4653) --- .../browser/unstable_batchedUpdates.test.js | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 compat/test/browser/unstable_batchedUpdates.test.js diff --git a/compat/test/browser/unstable_batchedUpdates.test.js b/compat/test/browser/unstable_batchedUpdates.test.js deleted file mode 100644 index 90d7f583a4..0000000000 --- a/compat/test/browser/unstable_batchedUpdates.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { unstable_batchedUpdates, flushSync } from 'preact/compat'; - -describe('unstable_batchedUpdates', () => { - it('should call the callback', () => { - const spy = sinon.spy(); - unstable_batchedUpdates(spy); - expect(spy).to.be.calledOnce; - }); - - it('should call callback with only one arg', () => { - const spy = sinon.spy(); - // @ts-expect-error - unstable_batchedUpdates(spy, 'foo', 'bar'); - expect(spy).to.be.calledWithExactly('foo'); - }); -}); - -describe('flushSync', () => { - it('should invoke the given callback', () => { - const returnValue = {}; - const spy = sinon.spy(() => returnValue); - const result = flushSync(spy); - expect(spy).to.have.been.calledOnce; - expect(result).to.equal(returnValue); - }); - - it('should invoke the given callback with the given argument', () => { - const returnValue = {}; - const spy = sinon.spy(() => returnValue); - const result = flushSync(spy, 'foo'); - expect(spy).to.be.calledWithExactly('foo'); - expect(result).to.equal(returnValue); - }); -}); From d837b75451077ecbf733bce93bd0979e05b19ec6 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 12 Feb 2025 09:47:40 +0100 Subject: [PATCH 11/38] Review feedback v11 (#4655) * Revert assign and avoid repeating indexed access * Check parentNode instead * Use flag * Remove type * Remove todo file --- TODO.md | 5 ----- src/diff/index.js | 12 ++++++------ src/index.d.ts | 10 ---------- src/render.js | 16 +++++++--------- src/util.js | 14 +++++++++++++- 5 files changed, 26 insertions(+), 31 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b358efbbc9..0000000000 --- a/TODO.md +++ /dev/null @@ -1,5 +0,0 @@ -- https://github.com/preactjs/preact/pull/4358 -- https://github.com/preactjs/preact/pull/4361 -- Remove deprecated lifecycle methods -- Put createPortal into core -- Implement hydration 2.0 diff --git a/src/diff/index.js b/src/diff/index.js index 57fa47ab00..df7257dedd 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -249,7 +249,7 @@ export function diff( c.state = c._nextState; if (c.getChildContext != NULL) { - globalContext = assign({}, globalContext, c.getChildContext()); + globalContext = assign(assign({}, globalContext), c.getChildContext()); } if (isClassComponent && !isNew && c.getSnapshotBeforeUpdate != NULL) { @@ -305,9 +305,8 @@ export function diff( newVNode._dom = oldDom; } else { for (let i = excessDomChildren.length; i--; ) { - if (excessDomChildren[i]) { - excessDomChildren[i].remove(); - } + const child = excessDomChildren[i]; + if (child) child.remove(); } } } else { @@ -564,7 +563,8 @@ function diffElementNodes( // Remove children that are not part of any vnode. if (excessDomChildren != NULL) { for (i = excessDomChildren.length; i--; ) { - if (excessDomChildren[i]) excessDomChildren[i].remove(); + const child = excessDomChildren[i]; + if (child) child.remove(); } } } @@ -663,7 +663,7 @@ export function unmount(vnode, parentVNode, skipRemove) { } } - if (!skipRemove && vnode._dom != null && typeof vnode.type != 'function') { + if (!skipRemove && vnode._dom != null && vnode._dom.parentNode) { vnode._dom.remove(); } diff --git a/src/index.d.ts b/src/index.d.ts index 08bc00fd25..92b5d49d27 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -299,16 +299,6 @@ interface ContainerNode { } export function render(vnode: ComponentChild, parent: ContainerNode): void; -/** - * @deprecated Will be removed in v11. - * - * Replacement Preact 10+ implementation can be found here: https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c - */ -export function render( - vnode: ComponentChild, - parent: ContainerNode, - replaceNode?: Element | Text -): void; export function hydrate(vnode: ComponentChild, parent: ContainerNode): void; export function cloneElement( vnode: VNode, diff --git a/src/render.js b/src/render.js index e1c60b6ba5..3817b7eacc 100644 --- a/src/render.js +++ b/src/render.js @@ -1,4 +1,4 @@ -import { EMPTY_OBJ, NULL } from './constants'; +import { EMPTY_OBJ, MODE_HYDRATE, NULL } from './constants'; import { commitRoot, diff } from './diff/index'; import { createElement, Fragment } from './create-element'; import options from './options'; @@ -8,10 +8,8 @@ import { slice } from './util'; * Render a Preact virtual node into a DOM element * @param {import('./internal').ComponentChild} vnode The virtual node to render * @param {import('./internal').PreactElement} parentDom The DOM element to render into - * @param {import('./internal').PreactElement | object} [replaceNode] Optional: Attempt to re-use an - * existing DOM tree rooted at `replaceNode` */ -export function render(vnode, parentDom, replaceNode) { +export function render(vnode, parentDom) { // https://github.com/preactjs/preact/issues/3794 if (parentDom == document) { parentDom = document.documentElement; @@ -19,10 +17,8 @@ export function render(vnode, parentDom, replaceNode) { if (options._root) options._root(vnode, parentDom); - // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in - // hydration mode or not by passing the `hydrate` function instead of a DOM - // element.. - let isHydrating = replaceNode === hydrate; + // @ts-expect-error + let isHydrating = !!(vnode && vnode._flags & MODE_HYDRATE); // To be able to support calling `render()` multiple times on the same // DOM node, we need to obtain a reference to the previous tree. We do @@ -65,5 +61,7 @@ export function render(vnode, parentDom, replaceNode) { * @param {import('./internal').PreactElement} parentDom The DOM element to update */ export function hydrate(vnode, parentDom) { - render(vnode, parentDom, hydrate); + // @ts-expect-error + vnode._flags |= MODE_HYDRATE; + render(vnode, parentDom); } diff --git a/src/util.js b/src/util.js index 8b112f69a2..282ab898d0 100644 --- a/src/util.js +++ b/src/util.js @@ -1,5 +1,17 @@ import { EMPTY_ARR } from './constants'; export const isArray = Array.isArray; -export const assign = Object.assign; export const slice = EMPTY_ARR.slice; + +/** + * Assign properties from `props` to `obj` + * @template O, P The obj and props types + * @param {O} obj The object to copy properties to + * @param {P} props The object to copy properties from + * @returns {O & P} + */ +export function assign(obj, props) { + // @ts-expect-error We change the type of `obj` to be `O & P` + for (let i in props) obj[i] = props[i]; + return /** @type {O & P} */ (obj); +} From 13ae428350267624ea8b01ea58eb3376e25fa804 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 12 Feb 2025 11:27:12 +0100 Subject: [PATCH 12/38] Forward ref by default (#4658) * Forward ref by default * Optimizations --- compat/src/forwardRef.js | 12 +- compat/src/internal.d.ts | 1 - compat/src/memo.js | 1 - compat/src/suspense.js | 1 - compat/test/browser/forwardRef.test.js | 3 +- compat/test/browser/memo.test.js | 13 +- compat/test/browser/suspense.test.js | 15 +- debug/src/debug.js | 11 +- hooks/test/browser/useContext.test.js | 6 +- jsx-runtime/src/index.js | 2 +- src/clone-element.js | 2 +- src/create-element.js | 2 +- test/browser/cloneElement.test.js | 3 +- test/browser/keys.test.js | 13 ++ .../lifecycles/componentDidCatch.test.js | 2 +- .../getDerivedStateFromError.test.js | 2 +- test/browser/placeholders.test.js | 5 + test/browser/refs.test.js | 199 +----------------- test/browser/render.test.js | 22 +- 19 files changed, 72 insertions(+), 243 deletions(-) diff --git a/compat/src/forwardRef.js b/compat/src/forwardRef.js index ddb6422883..1b46bab3ab 100644 --- a/compat/src/forwardRef.js +++ b/compat/src/forwardRef.js @@ -1,15 +1,5 @@ -import { options } from 'preact'; import { assign } from './util'; -let oldDiffHook = options._diff; -options._diff = vnode => { - if (vnode.type && vnode.type._forwarded && vnode.ref) { - vnode.props.ref = vnode.ref; - vnode.ref = null; - } - if (oldDiffHook) oldDiffHook(vnode); -}; - export const REACT_FORWARD_SYMBOL = Symbol.for('react.forward_ref'); /** @@ -34,7 +24,7 @@ export function forwardRef(fn) { // mobx-react throws. Forwarded.render = Forwarded; - Forwarded.prototype.isReactComponent = Forwarded._forwarded = true; + Forwarded.prototype.isReactComponent = true; Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; return Forwarded; } diff --git a/compat/src/internal.d.ts b/compat/src/internal.d.ts index 1573a5bf53..31795ba123 100644 --- a/compat/src/internal.d.ts +++ b/compat/src/internal.d.ts @@ -27,7 +27,6 @@ export interface Component

extends PreactComponent { export interface FunctionComponent

extends PreactFunctionComponent

{ shouldComponentUpdate?(nextProps: Readonly

): boolean; - _forwarded?: boolean; _patchedLifecycles?: true; } diff --git a/compat/src/memo.js b/compat/src/memo.js index e743199055..925e0c9eae 100644 --- a/compat/src/memo.js +++ b/compat/src/memo.js @@ -29,6 +29,5 @@ export function memo(c, comparer) { } Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; Memoed.prototype.isReactComponent = true; - Memoed._forwarded = true; return Memoed; } diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 5486ddbc18..f47da4ec30 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -271,6 +271,5 @@ export function lazy(loader) { } Lazy.displayName = 'Lazy'; - Lazy._forwarded = true; return Lazy; } diff --git a/compat/test/browser/forwardRef.test.js b/compat/test/browser/forwardRef.test.js index d9fe00e600..4e7968dd6d 100644 --- a/compat/test/browser/forwardRef.test.js +++ b/compat/test/browser/forwardRef.test.js @@ -402,8 +402,7 @@ describe('forwardRef', () => { const Transition = ({ children }) => { const state = useState(0); forceTransition = state[1]; - expect(children.ref).to.not.be.undefined; - if (state[0] === 0) expect(children.props.ref).to.be.undefined; + expect(children.props.ref).to.not.be.undefined; return children; }; diff --git a/compat/test/browser/memo.test.js b/compat/test/browser/memo.test.js index 0cbd6fe8cc..89063209e7 100644 --- a/compat/test/browser/memo.test.js +++ b/compat/test/browser/memo.test.js @@ -72,9 +72,9 @@ describe('memo()', () => { let ref = null; - function Foo() { + function Foo(props) { spy(); - return

Hello World

; + return

Hello World

; } let Memoized = memo(Foo); @@ -99,8 +99,7 @@ describe('memo()', () => { update(); rerender(); - expect(ref.current).not.to.be.undefined; - + expect(ref.current).to.equal(scratch.firstChild); // TODO: not sure whether this is in-line with react... expect(spy).to.be.calledTwice; }); @@ -175,8 +174,8 @@ describe('memo()', () => { it('should pass ref through nested memos', () => { class Foo extends Component { - render() { - return

Hello World

; + render(props) { + return

Hello World

; } } @@ -187,7 +186,7 @@ describe('memo()', () => { render(, scratch); expect(ref.current).not.to.be.undefined; - expect(ref.current).to.be.instanceOf(Foo); + expect(ref.current).to.equal(scratch.firstChild); }); it('should not unnecessarily reorder children #2895', () => { diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js index d18dc8104f..6b6024855f 100644 --- a/compat/test/browser/suspense.test.js +++ b/compat/test/browser/suspense.test.js @@ -272,7 +272,7 @@ describe('suspense', () => { }); it('lazy should forward refs', () => { - const LazyComp = () =>
Hello from LazyComp
; + const LazyComp = props =>
; let ref = {}; /** @type {() => Promise} */ @@ -298,7 +298,7 @@ describe('suspense', () => { return resolve().then(() => { rerender(); - expect(ref.current.constructor).to.equal(LazyComp); + expect(ref.current).to.equal(scratch.firstChild); }); }); @@ -1675,8 +1675,17 @@ describe('suspense', () => { // eslint-disable-next-line react/require-render-return class Suspender extends Component { + constructor(props) { + super(props); + this.state = { promise: new Promise(() => {}) }; + if (typeof props.ref === 'function') { + props.ref(this); + } else if (props.ref) { + props.ref.current = this; + } + } render() { - throw new Promise(() => {}); + throw this.state.promise; } } diff --git a/debug/src/debug.js b/debug/src/debug.js index 8da063130f..1d8eb7c0e6 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -11,7 +11,7 @@ import { getCurrentVNode, getDisplayName } from './component-stack'; -import { assign, isNaN } from './util'; +import { isNaN } from './util'; const isWeakMapSupported = typeof WeakMap == 'function'; @@ -229,15 +229,12 @@ export function initDebug() { } } - let values = vnode.props; - if (vnode.type._forwarded) { - values = assign({}, values); - delete values.ref; - } + /* eslint-disable-next-line */ + const { ref: _ref, ...props } = vnode.props; checkPropTypes( vnode.type.propTypes, - values, + props, 'prop', getDisplayName(vnode), () => getOwnerStack(vnode) diff --git a/hooks/test/browser/useContext.test.js b/hooks/test/browser/useContext.test.js index 6a47b107d8..be15e2b2dc 100644 --- a/hooks/test/browser/useContext.test.js +++ b/hooks/test/browser/useContext.test.js @@ -180,6 +180,7 @@ describe('useContext', () => { let provider, subSpy; function Comp() { + provider = this._vnode._parent._component; const value = useContext(Context); values.push(value); return null; @@ -188,7 +189,7 @@ describe('useContext', () => { render(, scratch); render( - (provider = p)} value={42}> + , scratch @@ -212,6 +213,7 @@ describe('useContext', () => { let provider, subSpy; function Comp() { + provider = this._vnode._parent._component; const value = useContext(Context); values.push(value); return null; @@ -220,7 +222,7 @@ describe('useContext', () => { render(, scratch); render( - (provider = p)} value={42}> + , scratch diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index 6bf06a06ec..7fa57b6264 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -35,7 +35,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { ref, i; - if ('ref' in normalizedProps) { + if ('ref' in normalizedProps && typeof type != 'function') { normalizedProps = {}; for (i in props) { if (i == 'ref') { diff --git a/src/clone-element.js b/src/clone-element.js index 241e9a706b..35ed9efa0a 100644 --- a/src/clone-element.js +++ b/src/clone-element.js @@ -25,7 +25,7 @@ export function cloneElement(vnode, props, children) { for (i in props) { if (i == 'key') key = props[i]; - else if (i == 'ref') ref = props[i]; + else if (i == 'ref' && typeof vnode.type != 'function') ref = props[i]; else if (props[i] == UNDEFINED && defaultProps != UNDEFINED) { normalizedProps[i] = defaultProps[i]; } else { diff --git a/src/create-element.js b/src/create-element.js index 39bebb70ba..b7130c367c 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -20,7 +20,7 @@ export function createElement(type, props, children) { i; for (i in props) { if (i == 'key') key = props[i]; - else if (i == 'ref') ref = props[i]; + else if (i == 'ref' && typeof type != 'function') ref = props[i]; else normalizedProps[i] = props[i]; } diff --git a/test/browser/cloneElement.test.js b/test/browser/cloneElement.test.js index 00d338efee..93b7218c88 100644 --- a/test/browser/cloneElement.test.js +++ b/test/browser/cloneElement.test.js @@ -63,8 +63,7 @@ describe('cloneElement', () => { it('should override ref if specified', () => { function a() {} function b() {} - function Foo() {} - const instance = hello; + const instance =
hello
; let clone = cloneElement(instance); expect(clone.ref).to.equal(a); diff --git a/test/browser/keys.test.js b/test/browser/keys.test.js index 5b11afbaa1..c7db54eb82 100644 --- a/test/browser/keys.test.js +++ b/test/browser/keys.test.js @@ -17,7 +17,20 @@ describe('keys', () => { function createStateful(name) { return class Stateful extends Component { + constructor(props) { + super(props); + if (typeof props.ref === 'function') { + props.ref(this); + } else if (props.ref) { + props.ref.current = this; + } + } componentDidUpdate() { + if (typeof this.props.ref === 'function') { + this.props.ref(this); + } else if (this.props.ref) { + this.props.ref.current = this; + } ops.push(`Update ${name}`); } componentDidMount() { diff --git a/test/browser/lifecycles/componentDidCatch.test.js b/test/browser/lifecycles/componentDidCatch.test.js index db11e0c2f4..5e91a5240f 100644 --- a/test/browser/lifecycles/componentDidCatch.test.js +++ b/test/browser/lifecycles/componentDidCatch.test.js @@ -324,7 +324,7 @@ describe('Lifecycle methods', () => { }); it('should be called when applying a Component ref', () => { - const Foo = () =>
; + const Foo = props =>
; const ref = value => { if (value) { diff --git a/test/browser/lifecycles/getDerivedStateFromError.test.js b/test/browser/lifecycles/getDerivedStateFromError.test.js index 4c279a83b3..318cf983b8 100644 --- a/test/browser/lifecycles/getDerivedStateFromError.test.js +++ b/test/browser/lifecycles/getDerivedStateFromError.test.js @@ -325,7 +325,7 @@ describe('Lifecycle methods', () => { }); it('should be called when applying a Component ref', () => { - const Foo = () =>
; + const Foo = props =>
; const ref = value => { if (value) { diff --git a/test/browser/placeholders.test.js b/test/browser/placeholders.test.js index a5c7bcb7b0..c721c8c908 100644 --- a/test/browser/placeholders.test.js +++ b/test/browser/placeholders.test.js @@ -117,6 +117,11 @@ describe('null placeholders', () => { class Stateful extends Component { constructor(props) { super(props); + if (typeof props.ref === 'function') { + props.ref(this); + } else if (props.ref) { + props.ref.current = this; + } this.state = { count: 0 }; } increment() { diff --git a/test/browser/refs.test.js b/test/browser/refs.test.js index cc59237975..5bdf6ff25d 100644 --- a/test/browser/refs.test.js +++ b/test/browser/refs.test.js @@ -71,21 +71,21 @@ describe('refs', () => { expect(inner).to.have.been.calledWith(scratch.firstChild.firstChild); }); - it('should pass components to ref functions', () => { + it('should pass component refs in props', () => { let ref = spy('ref'), instance; class Foo extends Component { constructor() { super(); - instance = this; } - render() { + render(props) { + instance = props.ref; return
; } } render(, scratch); - expect(ref).to.have.been.calledOnce.and.calledWith(instance); + expect(ref).to.equal(instance); }); it('should have a consistent order', () => { @@ -109,197 +109,6 @@ describe('refs', () => { ]); }); - it('should pass rendered DOM from functional components to ref functions', () => { - let ref = spy('ref'); - - const Foo = () =>
; - - render(, scratch); - expect(ref).to.have.been.calledOnce; - - ref.resetHistory(); - render(, scratch); - expect(ref).not.to.have.been.called; - - ref.resetHistory(); - render(, scratch); - expect(ref).to.have.been.calledOnce.and.calledWith(null); - }); - - it('should pass children to ref functions', () => { - let outer = spy('outer'), - inner = spy('inner'), - InnermostComponent = 'span', - /** @type {() => void} */ - update, - /** @type {Inner} */ - inst; - class Outer extends Component { - constructor() { - super(); - update = () => this.forceUpdate(); - } - render() { - return ( -
- -
- ); - } - } - class Inner extends Component { - constructor() { - super(); - inst = this; - } - render() { - return ; - } - } - - render(, scratch); - - expect(outer).to.have.been.calledOnce.and.calledWith(inst); - expect(inner).to.have.been.calledOnce.and.calledWith( - scratch.querySelector(InnermostComponent) - ); - - outer.resetHistory(); - inner.resetHistory(); - update(); - rerender(); - - expect(outer, 're-render').not.to.have.been.called; - expect(inner, 're-render').not.to.have.been.called; - - inner.resetHistory(); - InnermostComponent = 'x-span'; - update(); - rerender(); - - expect(inner, 're-render swap'); - expect(inner.firstCall, 're-render swap').to.have.been.calledWith(null); - expect(inner.secondCall, 're-render swap').to.have.been.calledWith( - scratch.querySelector(InnermostComponent) - ); - - InnermostComponent = 'span'; - outer.resetHistory(); - inner.resetHistory(); - render(
, scratch); - - expect(outer, 'unrender').to.have.been.calledOnce.and.calledWith(null); - expect(inner, 'unrender').to.have.been.calledOnce.and.calledWith(null); - }); - - it('should pass high-order children to ref functions', () => { - let outer = spy('outer'), - inner = spy('inner'), - innermost = spy('innermost'), - InnermostComponent = 'span', - outerInst, - /** @type {Inner} */ - innerInst; - class Outer extends Component { - constructor() { - super(); - outerInst = this; - } - render() { - return ; - } - } - class Inner extends Component { - constructor() { - super(); - innerInst = this; - } - render() { - return ; - } - } - - render(, scratch); - - expect(outer, 'outer initial').to.have.been.calledOnce.and.calledWith( - outerInst - ); - expect(inner, 'inner initial').to.have.been.calledOnce.and.calledWith( - innerInst - ); - expect( - innermost, - 'innerMost initial' - ).to.have.been.calledOnce.and.calledWith( - scratch.querySelector(InnermostComponent) - ); - - outer.resetHistory(); - inner.resetHistory(); - innermost.resetHistory(); - render(, scratch); - - expect(outer, 'outer update').not.to.have.been.called; - expect(inner, 'inner update').not.to.have.been.called; - expect(innermost, 'innerMost update').not.to.have.been.called; - - innermost.resetHistory(); - InnermostComponent = 'x-span'; - render(, scratch); - - expect(innermost, 'innerMost swap'); - expect(innermost.firstCall, 'innerMost swap').to.have.been.calledWith(null); - expect(innermost.secondCall, 'innerMost swap').to.have.been.calledWith( - scratch.querySelector(InnermostComponent) - ); - InnermostComponent = 'span'; - - outer.resetHistory(); - inner.resetHistory(); - innermost.resetHistory(); - render(
, scratch); - - expect(outer, 'outer unmount').to.have.been.calledOnce.and.calledWith(null); - expect(inner, 'inner unmount').to.have.been.calledOnce.and.calledWith(null); - expect( - innermost, - 'innerMost unmount' - ).to.have.been.calledOnce.and.calledWith(null); - }); - - // Test for #1143 - it('should not pass ref into component as a prop', () => { - let foo = spy('foo'), - bar = spy('bar'); - - class Foo extends Component { - render() { - return
; - } - } - const Bar = spy('Bar', () =>
); - - sinon.spy(Foo.prototype, 'render'); - - render( -
- - -
, - scratch - ); - - expect(Foo.prototype.render).to.have.been.calledWithMatch( - { ref: sinon.match.falsy, a: 'a' }, - {}, - {} - ); - expect(Bar).to.have.been.calledWithMatch( - { b: 'b', ref: sinon.match.falsy }, - {} - ); - }); - // Test for #232 it('should only null refs after unmount', () => { let outer, inner; diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 65c94e3b50..4d5562663c 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -716,7 +716,13 @@ describe('render()', () => { }); it('should avoid reapplying innerHTML when __html property of dangerouslySetInnerHTML attr remains unchanged', () => { + /** @type {Component} */ + let thing; class Thing extends Component { + constructor(props) { + super(props); + thing = this; + } render() { // eslint-disable-next-line react/no-danger return ( @@ -725,9 +731,7 @@ describe('render()', () => { } } - /** @type {Component} */ - let thing; - render( (thing = r)} />, scratch); + render(, scratch); let firstInnerHTMLChild = scratch.firstChild.firstChild; @@ -961,6 +965,10 @@ describe('render()', () => { let checkbox; class Inputs extends Component { + constructor(props) { + super(props); + inputs = this; + } render() { return (
@@ -971,7 +979,7 @@ describe('render()', () => { } } - render( (inputs = x)} />, scratch); + render(, scratch); expect(text.value).to.equal('Hello'); expect(checkbox.checked).to.equal(true); @@ -1100,9 +1108,12 @@ describe('render()', () => { // see preact/#1327 it('should not reuse unkeyed components', () => { + let ref; class X extends Component { constructor() { super(); + ref = this; + this.id = null; this.state = { i: 0 }; } @@ -1119,7 +1130,6 @@ describe('render()', () => { } } - let ref; /** @type {() => void} */ let updateApp; class App extends Component { @@ -1133,7 +1143,7 @@ describe('render()', () => { return (
{this.state.i === 0 && } - (ref = node)} /> +
); } From 22d060c7a754679d196c9d381565a104c5e4b297 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Wed, 12 Feb 2025 20:23:41 +0100 Subject: [PATCH 13/38] Use Object.is instead of the adhoc func --- compat/src/hooks.js | 3 +-- compat/src/util.js | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/compat/src/hooks.js b/compat/src/hooks.js index fbc2be592b..d0aeeb7d7c 100644 --- a/compat/src/hooks.js +++ b/compat/src/hooks.js @@ -1,5 +1,4 @@ import { useState, useLayoutEffect, useEffect } from 'preact/hooks'; -import { is } from './util'; /** * This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84 @@ -47,7 +46,7 @@ function didSnapshotChange(inst) { const prevValue = inst._value; try { const nextValue = latestGetSnapshot(); - return !is(prevValue, nextValue); + return !Object.is(prevValue, nextValue); } catch (error) { return true; } diff --git a/compat/src/util.js b/compat/src/util.js index 23d73fd916..5a5c11a363 100644 --- a/compat/src/util.js +++ b/compat/src/util.js @@ -11,14 +11,3 @@ export function shallowDiffers(a, b) { for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true; return false; } - -/** - * Check if two values are the same value - * @param {*} x - * @param {*} y - * @returns {boolean} - */ -export function is(x, y) { - // TODO: can we replace this with Object.is? - return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); -} From b3742a3ee3213857b883063bbb1b8d78bf23b258 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 13 Feb 2025 13:08:42 +0100 Subject: [PATCH 14/38] Move `defaultProps` into `preact/compat` (#4657) * Move `defaultProps` into `preact/compat` This will be handled in `options.vnode` for function/class components. This hook gets called for every invocation of `jsx`/`createElement` and `cloneElement`. * Try it * refactor: This is horrific but seems to work? (#4662) --------- Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com> --- compat/src/index.d.ts | 209 ++++++++++++++----- compat/src/render.js | 10 +- compat/test/browser/component.test.js | 87 +++++++- compat/test/ts/index.tsx | 2 + jsx-runtime/src/index.js | 9 - jsx-runtime/test/browser/jsx-runtime.test.js | 34 --- src/clone-element.js | 14 +- src/create-element.js | 3 + src/index-5.d.ts | 3 - src/index.d.ts | 3 - src/internal.d.ts | 7 +- test/browser/cloneElement.test.js | 15 +- test/browser/spec.test.js | 83 -------- test/shared/createElement.test.js | 12 -- 14 files changed, 264 insertions(+), 227 deletions(-) diff --git a/compat/src/index.d.ts b/compat/src/index.d.ts index 15b5bf7c2b..41f593c5a1 100644 --- a/compat/src/index.d.ts +++ b/compat/src/index.d.ts @@ -1,10 +1,114 @@ import * as _hooks from '../../hooks'; // Intentionally not using a relative path to take advantage of // the TS version resolution mechanism -import * as preact from 'preact'; +import * as preact1 from 'preact'; import { JSXInternal } from '../../src/jsx'; import * as _Suspense from './suspense'; +<<<<<<< HEAD +======= +interface SignalLike { + value: T; + peek(): T; + subscribe(fn: (value: T) => void): () => void; +} + +type Signalish = T | SignalLike; + +declare namespace preact { + export interface FunctionComponent

{ + ( + props: preact1.RenderableProps

, + context?: any + ): preact1.ComponentChildren; + displayName?: string; + defaultProps?: Partial

| undefined; + } + + export interface ComponentClass

{ + new (props: P, context?: any): preact1.Component; + displayName?: string; + defaultProps?: Partial

; + contextType?: preact1.Context; + getDerivedStateFromProps?( + props: Readonly

, + state: Readonly + ): Partial | null; + getDerivedStateFromError?(error: any): Partial | null; + } + + export interface Component

{ + componentWillMount?(): void; + componentDidMount?(): void; + componentWillUnmount?(): void; + getChildContext?(): object; + componentWillReceiveProps?(nextProps: Readonly

, nextContext: any): void; + shouldComponentUpdate?( + nextProps: Readonly

, + nextState: Readonly, + nextContext: any + ): boolean; + componentWillUpdate?( + nextProps: Readonly

, + nextState: Readonly, + nextContext: any + ): void; + getSnapshotBeforeUpdate?(oldProps: Readonly

, oldState: Readonly): any; + componentDidUpdate?( + previousProps: Readonly

, + previousState: Readonly, + snapshot: any + ): void; + componentDidCatch?(error: any, errorInfo: preact1.ErrorInfo): void; + } + + export abstract class Component { + constructor(props?: P, context?: any); + + static displayName?: string; + static defaultProps?: any; + static contextType?: preact1.Context; + + // Static members cannot reference class type parameters. This is not + // supported in TypeScript. Reusing the same type arguments from `Component` + // will lead to an impossible state where one cannot satisfy the type + // constraint under no circumstances, see #1356.In general type arguments + // seem to be a bit buggy and not supported well at the time of this + // writing with TS 3.3.3333. + static getDerivedStateFromProps?( + props: Readonly, + state: Readonly + ): object | null; + static getDerivedStateFromError?(error: any): object | null; + + state: Readonly; + props: preact1.RenderableProps

; + context: any; + + // From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402 + // // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. + // // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 + setState( + state: + | (( + prevState: Readonly, + props: Readonly

+ ) => Pick | Partial | null) + | (Pick | Partial | null), + callback?: () => void + ): void; + + forceUpdate(callback?: () => void): void; + + abstract render( + props?: preact1.RenderableProps

, + state?: Readonly, + context?: any + ): preact1.ComponentChildren; + } +} + +>>>>>>> Move `defaultProps` into `preact/compat` (#4657) // export default React; export = React; export as namespace React; @@ -41,32 +145,32 @@ declare namespace React { ): T; // Preact Defaults - export import Context = preact.Context; - export import ContextType = preact.ContextType; - export import RefObject = preact.RefObject; + export import Context = preact1.Context; + export import ContextType = preact1.ContextType; + export import RefObject = preact1.RefObject; export import Component = preact.Component; export import FunctionComponent = preact.FunctionComponent; - export import ComponentType = preact.ComponentType; + export import ComponentType = preact1.ComponentType; export import ComponentClass = preact.ComponentClass; - export import FC = preact.FunctionComponent; - export import createContext = preact.createContext; - export import Ref = preact.Ref; - export import createRef = preact.createRef; - export import Fragment = preact.Fragment; - export import createElement = preact.createElement; - export import cloneElement = preact.cloneElement; - export import ComponentProps = preact.ComponentProps; - export import ReactNode = preact.ComponentChild; - export import ReactElement = preact.VNode; - export import Consumer = preact.Consumer; - export import ErrorInfo = preact.ErrorInfo; + export import FC = preact1.FunctionComponent; + export import createContext = preact1.createContext; + export import Ref = preact1.Ref; + export import createRef = preact1.createRef; + export import Fragment = preact1.Fragment; + export import createElement = preact1.createElement; + export import cloneElement = preact1.cloneElement; + export import ComponentProps = preact1.ComponentProps; + export import ReactNode = preact1.ComponentChild; + export import ReactElement = preact1.VNode; + export import Consumer = preact1.Consumer; + export import ErrorInfo = preact1.ErrorInfo; // Suspense export import Suspense = _Suspense.Suspense; export import lazy = _Suspense.lazy; // Compat - export import StrictMode = preact.Fragment; + export import StrictMode = preact1.Fragment; export const version: string; export function startTransition(cb: () => void): void; @@ -75,7 +179,7 @@ declare namespace React { extends JSXInternal.HTMLAttributes {} export interface HTMLProps extends JSXInternal.AllHTMLAttributes, - preact.ClassAttributes {} + preact1.ClassAttributes {} export interface AllHTMLAttributes extends JSXInternal.AllHTMLAttributes {} export import DetailedHTMLProps = JSXInternal.DetailedHTMLProps; @@ -83,7 +187,7 @@ declare namespace React { export interface SVGProps extends JSXInternal.SVGAttributes, - preact.ClassAttributes {} + preact1.ClassAttributes {} interface SVGAttributes extends JSXInternal.SVGAttributes {} @@ -181,73 +285,73 @@ declare namespace React { export import TransitionEventHandler = JSXInternal.TransitionEventHandler; export function createPortal( - vnode: preact.ComponentChildren, - container: preact.ContainerNode - ): preact.VNode; + vnode: preact1.ComponentChildren, + container: preact1.ContainerNode + ): preact1.VNode; export function render( - vnode: preact.ComponentChild, - parent: preact.ContainerNode, + vnode: preact1.ComponentChild, + parent: preact1.ContainerNode, callback?: () => void ): Component | null; export function hydrate( - vnode: preact.ComponentChild, - parent: preact.ContainerNode, + vnode: preact1.ComponentChild, + parent: preact1.ContainerNode, callback?: () => void ): Component | null; export function unmountComponentAtNode( - container: preact.ContainerNode + container: preact1.ContainerNode ): boolean; export function createFactory( - type: preact.VNode['type'] + type: preact1.VNode['type'] ): ( props?: any, - ...children: preact.ComponentChildren[] - ) => preact.VNode; + ...children: preact1.ComponentChildren[] + ) => preact1.VNode; export function isValidElement(element: any): boolean; export function isFragment(element: any): boolean; export function isMemo(element: any): boolean; export function findDOMNode( - component: preact.Component | Element + component: preact1.Component | Element ): Element | null; export abstract class PureComponent< P = {}, S = {}, SS = any - > extends preact.Component { + > extends preact1.Component { isPureReactComponent: boolean; } - export type MemoExoticComponent> = - preact.FunctionComponent> & { + export type MemoExoticComponent> = + preact1.FunctionComponent> & { readonly type: C; }; export function memo

( - component: preact.FunctionalComponent

, + component: preact1.FunctionalComponent

, comparer?: (prev: P, next: P) => boolean - ): preact.FunctionComponent

; - export function memo>( + ): preact1.FunctionComponent

; + export function memo>( component: C, comparer?: ( - prev: preact.ComponentProps, - next: preact.ComponentProps + prev: preact1.ComponentProps, + next: preact1.ComponentProps ) => boolean ): C; - export interface RefAttributes extends preact.Attributes { - ref?: preact.Ref | undefined; + export interface RefAttributes extends preact1.Attributes { + ref?: preact1.Ref | undefined; } /** * @deprecated Please use `ForwardRefRenderFunction` instead. */ export interface ForwardFn

{ - (props: P, ref: ForwardedRef): preact.ComponentChild; + (props: P, ref: ForwardedRef): preact1.ComponentChild; displayName?: string; } @@ -257,13 +361,18 @@ declare namespace React { } export interface ForwardRefExoticComponent

- extends preact.FunctionComponent

{ + extends preact1.FunctionComponent

{ defaultProps?: Partial

| undefined; } export function forwardRef( +<<<<<<< HEAD fn: ForwardRefRenderFunction ): preact.FunctionalComponent & { ref?: preact.Ref }>; +======= + fn: ForwardFn + ): preact1.FunctionalComponent & { ref?: preact1.Ref }>; +>>>>>>> Move `defaultProps` into `preact/compat` (#4657) export type PropsWithoutRef

= Omit; @@ -311,21 +420,21 @@ declare namespace React { export function flushSync(fn: (a: A) => R, a: A): R; export type PropsWithChildren

= P & { - children?: preact.ComponentChildren | undefined; + children?: preact1.ComponentChildren | undefined; }; export const Children: { - map( + map( children: T | T[], fn: (child: T, i: number) => R ): R[]; - forEach( + forEach( children: T | T[], fn: (child: T, i: number) => void ): void; - count: (children: preact.ComponentChildren) => number; - only: (children: preact.ComponentChildren) => preact.ComponentChild; - toArray: (children: preact.ComponentChildren) => preact.VNode<{}>[]; + count: (children: preact1.ComponentChildren) => number; + only: (children: preact1.ComponentChildren) => preact1.ComponentChild; + toArray: (children: preact1.ComponentChildren) => preact1.VNode<{}>[]; }; // scheduler diff --git a/compat/src/render.js b/compat/src/render.js index 302d19589e..785c4faa84 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -24,6 +24,7 @@ import { useSyncExternalStore, useTransition } from './index'; +import { assign } from './util'; export const REACT_ELEMENT_TYPE = Symbol.for('react.element'); @@ -237,8 +238,15 @@ options.vnode = vnode => { // only normalize props on Element nodes if (typeof vnode.type === 'string') { handleDomVNode(vnode); + } else if (typeof vnode.type === 'function' && vnode.type.defaultProps) { + let normalizedProps = assign({}, vnode.props); + for (let i in vnode.type.defaultProps) { + if (normalizedProps[i] === undefined) { + normalizedProps[i] = vnode.type.defaultProps[i]; + } + } + vnode.props = normalizedProps; } - vnode.$$typeof = REACT_ELEMENT_TYPE; if (oldVNodeHook) oldVNodeHook(vnode); diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js index de78a1fd20..8ca3b2cc32 100644 --- a/compat/test/browser/component.test.js +++ b/compat/test/browser/component.test.js @@ -1,6 +1,6 @@ import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; -import React, { createElement } from 'preact/compat'; +import React, { createElement, Component } from 'preact/compat'; describe('components', () => { /** @type {HTMLDivElement} */ @@ -240,4 +240,89 @@ describe('components', () => { expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called; }); }); + + describe('defaultProps', () => { + it('should apply default props on initial render', () => { + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + expect(props).to.be.deep.equal({ + fieldA: 1, + fieldB: 2, + fieldC: 1, + fieldD: 2 + }); + } + render() { + return

; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + React.render( + , + scratch + ); + }); + + it('should apply default props on rerender', () => { + let doRender; + class Outer extends Component { + constructor() { + super(); + this.state = { i: 1 }; + } + componentDidMount() { + doRender = () => this.setState({ i: 2 }); + } + render(props, { i }) { + return ; + } + } + class WithDefaultProps extends Component { + constructor(props, context) { + super(props, context); + this.ctor(props, context); + } + ctor() {} + componentWillReceiveProps() {} + render() { + return
; + } + } + WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; + + let proto = WithDefaultProps.prototype; + sinon.spy(proto, 'ctor'); + sinon.spy(proto, 'componentWillReceiveProps'); + sinon.spy(proto, 'render'); + + React.render(, scratch); + doRender(); + + const PROPS1 = { + fieldA: 1, + fieldB: 1, + fieldC: 1, + fieldD: 1 + }; + + const PROPS2 = { + fieldA: 1, + fieldB: 2, + fieldC: 1, + fieldD: 2 + }; + + expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); + expect(proto.render).to.have.been.calledWithMatch(PROPS1); + + rerender(); + + // expect(proto.ctor).to.have.been.calledWith(PROPS2); + expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch( + PROPS2 + ); + expect(proto.render).to.have.been.calledWithMatch(PROPS2); + }); + }); }); diff --git a/compat/test/ts/index.tsx b/compat/test/ts/index.tsx index 2e322931d9..51dbd60adc 100644 --- a/compat/test/ts/index.tsx +++ b/compat/test/ts/index.tsx @@ -34,6 +34,8 @@ class SimpleComponentWithContextAsProvider extends React.Component { } } +SimpleComponentWithContextAsProvider.defaultProps = { foo: 'default' }; + React.render( , document.createElement('div') diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index 7fa57b6264..0d05306ecd 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -65,15 +65,6 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { __self }; - // If a Component VNode, check for and apply defaultProps. - // Note: `type` is often a String, and can be `undefined` in development. - if (typeof type === 'function' && (ref = type.defaultProps)) { - for (i in ref) - if (typeof normalizedProps[i] === 'undefined') { - normalizedProps[i] = ref[i]; - } - } - if (options.vnode) options.vnode(vnode); return vnode; } diff --git a/jsx-runtime/test/browser/jsx-runtime.test.js b/jsx-runtime/test/browser/jsx-runtime.test.js index ac678ce477..0c6a8fae72 100644 --- a/jsx-runtime/test/browser/jsx-runtime.test.js +++ b/jsx-runtime/test/browser/jsx-runtime.test.js @@ -51,40 +51,6 @@ describe('Babel jsx/jsxDEV', () => { expect(vnode.key).to.equal('foo'); }); - it('should apply defaultProps', () => { - class Foo extends Component { - render() { - return
; - } - } - - Foo.defaultProps = { - foo: 'bar' - }; - - const vnode = jsx(Foo, {}, null); - expect(vnode.props).to.deep.equal({ - foo: 'bar' - }); - }); - - it('should keep props over defaultProps', () => { - class Foo extends Component { - render() { - return
; - } - } - - Foo.defaultProps = { - foo: 'bar' - }; - - const vnode = jsx(Foo, { foo: 'baz' }, null); - expect(vnode.props).to.deep.equal({ - foo: 'baz' - }); - }); - it('should set __source and __self', () => { const vnode = jsx('div', { class: 'foo' }, 'key', false, 'source', 'self'); expect(vnode.__source).to.equal('source'); diff --git a/src/clone-element.js b/src/clone-element.js index 35ed9efa0a..b2f6e118cf 100644 --- a/src/clone-element.js +++ b/src/clone-element.js @@ -1,6 +1,6 @@ import { assign, slice } from './util'; import { createVNode } from './create-element'; -import { NULL, UNDEFINED } from './constants'; +import { NULL } from './constants'; /** * Clones the given VNode, optionally adding attributes/props and replacing its @@ -17,20 +17,10 @@ export function cloneElement(vnode, props, children) { ref, i; - let defaultProps; - - if (vnode.type && vnode.type.defaultProps) { - defaultProps = vnode.type.defaultProps; - } - for (i in props) { if (i == 'key') key = props[i]; else if (i == 'ref' && typeof vnode.type != 'function') ref = props[i]; - else if (props[i] == UNDEFINED && defaultProps != UNDEFINED) { - normalizedProps[i] = defaultProps[i]; - } else { - normalizedProps[i] = props[i]; - } + else normalizedProps[i] = props[i]; } if (arguments.length > 2) { diff --git a/src/create-element.js b/src/create-element.js index b7130c367c..520f86374a 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -29,6 +29,7 @@ export function createElement(type, props, children) { arguments.length > 3 ? slice.call(arguments, 2) : children; } +<<<<<<< HEAD // If a Component VNode, check for and apply defaultProps // Note: type may be undefined in development, must never error here. if (typeof type == 'function' && type.defaultProps != NULL) { @@ -39,6 +40,8 @@ export function createElement(type, props, children) { } } +======= +>>>>>>> 04457d6e (Move `defaultProps` into `preact/compat` (#4657)) return createVNode(type, normalizedProps, key, ref, NULL); } diff --git a/src/index-5.d.ts b/src/index-5.d.ts index 6c7431cb00..8bf29da0f6 100644 --- a/src/index-5.d.ts +++ b/src/index-5.d.ts @@ -89,14 +89,12 @@ export type ComponentProps< export interface FunctionComponent

{ (props: RenderableProps

, context?: any): VNode | null; displayName?: string; - defaultProps?: Partial

| undefined; } export interface FunctionalComponent

extends FunctionComponent

{} export interface ComponentClass

{ new (props: P, context?: any): Component; displayName?: string; - defaultProps?: Partial

; contextType?: Context; getDerivedStateFromProps?( props: Readonly

, @@ -141,7 +139,6 @@ export abstract class Component { constructor(props?: P, context?: any); static displayName?: string; - static defaultProps?: any; static contextType?: Context; // Static members cannot reference class type parameters. This is not diff --git a/src/index.d.ts b/src/index.d.ts index 92b5d49d27..287d575c53 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -89,14 +89,12 @@ export type ComponentProps< export interface FunctionComponent

{ (props: RenderableProps

, context?: any): ComponentChildren; displayName?: string; - defaultProps?: Partial

| undefined; } export interface FunctionalComponent

extends FunctionComponent

{} export interface ComponentClass

{ new (props: P, context?: any): Component; displayName?: string; - defaultProps?: Partial

; contextType?: Context; getDerivedStateFromProps?( props: Readonly

, @@ -141,7 +139,6 @@ export abstract class Component { constructor(props?: P, context?: any); static displayName?: string; - static defaultProps?: any; static contextType?: Context; // Static members cannot reference class type parameters. This is not diff --git a/src/internal.d.ts b/src/internal.d.ts index 7a78d668a4..1c64329b4a 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -62,8 +62,7 @@ export type ComponentChild = | undefined; export type ComponentChildren = ComponentChild[] | ComponentChild; -export interface FunctionComponent

- extends preact.FunctionComponent

{ +export interface FunctionComponent

extends preact.FunctionComponent

{ // Internally, createContext uses `contextType` on a Function component to // implement the Consumer component contextType?: PreactContext; @@ -140,9 +139,7 @@ type RefCallback = { export type Ref = RefObject | RefCallback; export interface VNode

extends preact.VNode

{ - // Redefine type here using our internal ComponentType type, and specify - // string has an undefined `defaultProps` property to make TS happy - type: (string & { defaultProps: undefined }) | ComponentType

; + type: string | ComponentType

; props: P & { children: ComponentChildren }; ref?: Ref | null; _children: Array> | null; diff --git a/test/browser/cloneElement.test.js b/test/browser/cloneElement.test.js index 93b7218c88..fb8d3367c5 100644 --- a/test/browser/cloneElement.test.js +++ b/test/browser/cloneElement.test.js @@ -1,4 +1,4 @@ -import { createElement, cloneElement, createRef, Component } from 'preact'; +import { createElement, cloneElement, createRef } from 'preact'; /** @jsx createElement */ @@ -83,17 +83,4 @@ describe('cloneElement', () => { const clone = cloneElement(div, { key: 'myKey' }); expect(clone.props.key).to.equal(undefined); }); - - it('should prevent undefined properties from overriding default props', () => { - class Example extends Component { - render(props) { - return

thing
; - } - } - Example.defaultProps = { color: 'blue' }; - - const element = ; - const clone = cloneElement(element, { color: undefined }); - expect(clone.props.color).to.equal('blue'); - }); }); diff --git a/test/browser/spec.test.js b/test/browser/spec.test.js index 50f4a6e786..2511f803d5 100644 --- a/test/browser/spec.test.js +++ b/test/browser/spec.test.js @@ -16,89 +16,6 @@ describe('Component spec', () => { teardown(scratch); }); - describe('defaultProps', () => { - it('should apply default props on initial render', () => { - class WithDefaultProps extends Component { - constructor(props, context) { - super(props, context); - expect(props).to.be.deep.equal({ - fieldA: 1, - fieldB: 2, - fieldC: 1, - fieldD: 2 - }); - } - render() { - return
; - } - } - WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; - render(, scratch); - }); - - it('should apply default props on rerender', () => { - /** @type {() => void} */ - let doRender; - class Outer extends Component { - constructor() { - super(); - this.state = { i: 1 }; - } - componentDidMount() { - doRender = () => this.setState({ i: 2 }); - } - render(props, { i }) { - return ; - } - } - class WithDefaultProps extends Component { - constructor(props, context) { - super(props, context); - this.ctor(props, context); - } - ctor() {} - componentWillReceiveProps() {} - render() { - return
; - } - } - WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; - - let proto = WithDefaultProps.prototype; - sinon.spy(proto, 'ctor'); - sinon.spy(proto, 'componentWillReceiveProps'); - sinon.spy(proto, 'render'); - - render(, scratch); - doRender(); - - const PROPS1 = { - fieldA: 1, - fieldB: 1, - fieldC: 1, - fieldD: 1 - }; - - const PROPS2 = { - fieldA: 1, - fieldB: 2, - fieldC: 1, - fieldD: 2 - }; - - expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); - expect(proto.render).to.have.been.calledWithMatch(PROPS1); - - rerender(); - - // expect(proto.ctor).to.have.been.calledWith(PROPS2); - expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch( - PROPS2 - ); - expect(proto.render).to.have.been.calledWithMatch(PROPS2); - }); - }); - describe('forceUpdate', () => { it('should force a rerender', () => { /** @type {() => void} */ diff --git a/test/shared/createElement.test.js b/test/shared/createElement.test.js index 57394d857e..1b63d8c5de 100644 --- a/test/shared/createElement.test.js +++ b/test/shared/createElement.test.js @@ -265,18 +265,6 @@ describe('createElement(jsx)', () => { .that.deep.equals(['x', 'y']); }); - it('should respect defaultProps', () => { - const Component = ({ children }) => children; - Component.defaultProps = { foo: 'bar' }; - expect(h(Component, null).props).to.deep.equal({ foo: 'bar' }); - }); - - it('should override defaultProps', () => { - const Component = ({ children }) => children; - Component.defaultProps = { foo: 'default' }; - expect(h(Component, { foo: 'bar' }).props).to.deep.equal({ foo: 'bar' }); - }); - it('should ignore props.children if children are manually specified', () => { const element = (
From de18b1c72abdcedeac551ac594f51947ba33deed Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 13 Feb 2025 17:22:59 +0100 Subject: [PATCH 15/38] Look at impact of removing deprecated lifecycles (#4656) --- compat/src/render.js | 27 --- compat/test/browser/component.test.js | 250 -------------------------- 2 files changed, 277 deletions(-) diff --git a/compat/src/render.js b/compat/src/render.js index 785c4faa84..18fb74319b 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -40,33 +40,6 @@ const onChangeInputType = type => /fil|che|rad/.test(type); // Some libraries like `react-virtualized` explicitly check for this. Component.prototype.isReactComponent = {}; -// `UNSAFE_*` lifecycle hooks -// Preact only ever invokes the unprefixed methods. -// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method. -// - If a component defines its own `componentDidMount()` (including via defineProperty), use that. -// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter. -// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property. -// See https://github.com/preactjs/preact/issues/1941 -[ - 'componentWillMount', - 'componentWillReceiveProps', - 'componentWillUpdate' -].forEach(key => { - Object.defineProperty(Component.prototype, key, { - configurable: true, - get() { - return this['UNSAFE_' + key]; - }, - set(v) { - Object.defineProperty(this, key, { - configurable: true, - writable: true, - value: v - }); - } - }); -}); - /** * Proxy render() since React returns a Component reference. * @param {import('./internal').VNode} vnode VNode tree to render diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js index 8ca3b2cc32..83aec28873 100644 --- a/compat/test/browser/component.test.js +++ b/compat/test/browser/component.test.js @@ -75,254 +75,4 @@ describe('components', () => { children: 'second' }); }); - - describe('UNSAFE_* lifecycle methods', () => { - it('should support UNSAFE_componentWillMount', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - spy(); - } - - render() { - return

foo

; - } - } - - React.render(, scratch); - - expect(spy).to.be.calledOnce; - }); - - it('should support UNSAFE_componentWillMount #2', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - render() { - return

foo

; - } - } - - Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillMount', { - value: spy - }); - - React.render(, scratch); - expect(spy).to.be.calledOnce; - }); - - it('should support UNSAFE_componentWillReceiveProps', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps() { - spy(); - } - - render() { - return

foo

; - } - } - - React.render(, scratch); - // Trigger an update - React.render(, scratch); - expect(spy).to.be.calledOnce; - }); - - it('should support UNSAFE_componentWillReceiveProps #2', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - render() { - return

foo

; - } - } - - Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillReceiveProps', { - value: spy - }); - - React.render(, scratch); - // Trigger an update - React.render(, scratch); - expect(spy).to.be.calledOnce; - }); - - it('should support UNSAFE_componentWillUpdate', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - // eslint-disable-next-line camelcase - UNSAFE_componentWillUpdate() { - spy(); - } - - render() { - return

foo

; - } - } - - React.render(, scratch); - // Trigger an update - React.render(, scratch); - expect(spy).to.be.calledOnce; - }); - - it('should support UNSAFE_componentWillUpdate #2', () => { - let spy = sinon.spy(); - - class Foo extends React.Component { - render() { - return

foo

; - } - } - - Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillUpdate', { - value: spy - }); - - React.render(, scratch); - // Trigger an update - React.render(, scratch); - expect(spy).to.be.calledOnce; - }); - - it('should alias UNSAFE_* method to non-prefixed variant', () => { - let inst; - class Foo extends React.Component { - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() {} - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps() {} - // eslint-disable-next-line camelcase - UNSAFE_componentWillUpdate() {} - render() { - inst = this; - return
foo
; - } - } - - React.render(, scratch); - - expect(inst.UNSAFE_componentWillMount).to.equal(inst.componentWillMount); - expect(inst.UNSAFE_componentWillReceiveProps).to.equal( - inst.UNSAFE_componentWillReceiveProps - ); - expect(inst.UNSAFE_componentWillUpdate).to.equal( - inst.UNSAFE_componentWillUpdate - ); - }); - - it('should call UNSAFE_* methods through Suspense with wrapper component #2525', () => { - class Page extends React.Component { - UNSAFE_componentWillMount() {} - render() { - return

Example

; - } - } - - const Wrapper = () => ; - - sinon.spy(Page.prototype, 'UNSAFE_componentWillMount'); - - React.render( - fallback
}> - - , - scratch - ); - - expect(scratch.innerHTML).to.equal('

Example

'); - expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called; - }); - }); - - describe('defaultProps', () => { - it('should apply default props on initial render', () => { - class WithDefaultProps extends Component { - constructor(props, context) { - super(props, context); - expect(props).to.be.deep.equal({ - fieldA: 1, - fieldB: 2, - fieldC: 1, - fieldD: 2 - }); - } - render() { - return
; - } - } - WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; - React.render( - , - scratch - ); - }); - - it('should apply default props on rerender', () => { - let doRender; - class Outer extends Component { - constructor() { - super(); - this.state = { i: 1 }; - } - componentDidMount() { - doRender = () => this.setState({ i: 2 }); - } - render(props, { i }) { - return ; - } - } - class WithDefaultProps extends Component { - constructor(props, context) { - super(props, context); - this.ctor(props, context); - } - ctor() {} - componentWillReceiveProps() {} - render() { - return
; - } - } - WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }; - - let proto = WithDefaultProps.prototype; - sinon.spy(proto, 'ctor'); - sinon.spy(proto, 'componentWillReceiveProps'); - sinon.spy(proto, 'render'); - - React.render(, scratch); - doRender(); - - const PROPS1 = { - fieldA: 1, - fieldB: 1, - fieldC: 1, - fieldD: 1 - }; - - const PROPS2 = { - fieldA: 1, - fieldB: 2, - fieldC: 1, - fieldD: 2 - }; - - expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); - expect(proto.render).to.have.been.calledWithMatch(PROPS1); - - rerender(); - - // expect(proto.ctor).to.have.been.calledWith(PROPS2); - expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch( - PROPS2 - ); - expect(proto.render).to.have.been.calledWithMatch(PROPS2); - }); - }); }); From e14cbd4adbaa78342221832f93b53bc49f864083 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 13 Feb 2025 17:28:03 +0100 Subject: [PATCH 16/38] Remove unused imports --- compat/test/browser/component.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js index 83aec28873..9aecf754bc 100644 --- a/compat/test/browser/component.test.js +++ b/compat/test/browser/component.test.js @@ -1,6 +1,6 @@ import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; -import React, { createElement, Component } from 'preact/compat'; +import React, { createElement } from 'preact/compat'; describe('components', () => { /** @type {HTMLDivElement} */ From fa130af01c0c44c00d5f500496a7a19cf8e9db4b Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:32:35 -0600 Subject: [PATCH 17/38] fix: Mangle `_listeners` as `__l` instead of `l` (#4463) --- mangle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mangle.json b/mangle.json index 2d15afef53..880aeafdaa 100644 --- a/mangle.json +++ b/mangle.json @@ -26,7 +26,7 @@ "cname": 6, "props": { "$_hasScuFromHooks": "__f", - "$_listeners": "l", + "$_listeners": "__l", "$_cleanup": "__c", "$__hooks": "__H", "$_hydrationMismatch": "__m", From d0fcc65d8ec03221f5fe3060a54d3392eae847c7 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 13 Feb 2025 17:41:10 +0100 Subject: [PATCH 18/38] Comment denoted hydration (#4636) * Remove unused imports * Comment denoted hydration * Make it work * Golfies --- .../test/browser/suspense-hydration.test.js | 292 ++++++++++++++---- src/diff/index.js | 51 ++- src/internal.d.ts | 1 + 3 files changed, 283 insertions(+), 61 deletions(-) diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js index b364cf41c6..befba4972f 100644 --- a/compat/test/browser/suspense-hydration.test.js +++ b/compat/test/browser/suspense-hydration.test.js @@ -723,61 +723,17 @@ describe('suspense hydration', () => { }); }); - // Currently not supported. Hydration doesn't set attributes... but should it - // when coming back from suspense if props were updated? - it.skip('should hydrate and update attributes with latest props', () => { - const originalHtml = '

Count: 0

Lazy count: 0

'; - scratch.innerHTML = originalHtml; - clearLog(); - - /** @type {() => void} */ - let increment; - const [Lazy, resolve] = createLazy(); - function App() { - const [count, setCount] = useState(0); - increment = () => setCount(c => c + 1); - - return ( - -

Count: {count}

- -
- ); - } - - hydrate(, scratch); - rerender(); // Flush rerender queue to mimic what preact will really do - expect(scratch.innerHTML).to.equal(originalHtml); - // Re: DOM OP below - Known issue with hydrating merged text nodes - expect(getLog()).to.deep.equal(['

Count: .appendChild(#text)']); - clearLog(); - - increment(); - rerender(); - - expect(scratch.innerHTML).to.equal( - '

Count: 1

Lazy count: 0

' - ); - expect(getLog()).to.deep.equal([]); - clearLog(); - - return resolve(({ count }) => ( -

Lazy count: {count}

- )).then(() => { - rerender(); - expect(scratch.innerHTML).to.equal( - '

Count: 1

Lazy count: 1

' - ); - // Re: DOM OP below - Known issue with hydrating merged text nodes - expect(getLog()).to.deep.equal(['

Lazy count: .appendChild(#text)']); - clearLog(); - }); - }); - - // Currently not supported, but I wrote the test before I realized that so - // leaving it here in case we do support it eventually - it.skip('should properly hydrate suspense when resolves to a Fragment', () => { - const originalHtml = ul([li(0), li(1), li(2), li(3), li(4), li(5)]); + it('should properly hydrate suspense when resolves to a Fragment', () => { + const originalHtml = ul([ + li(0), + li(1), + '', + li(2), + li(3), + '', + li(4), + li(5) + ]); const listeners = [ sinon.spy(), @@ -809,8 +765,8 @@ describe('suspense hydration', () => { scratch ); rerender(); // Flush rerender queue to mimic what preact will really do - expect(scratch.innerHTML).to.equal(originalHtml); expect(getLog()).to.deep.equal([]); + expect(scratch.innerHTML).to.equal(originalHtml); expect(listeners[5]).not.to.have.been.called; clearLog(); @@ -839,4 +795,228 @@ describe('suspense hydration', () => { expect(listeners[5]).to.have.been.calledTwice; }); }); + + it('should properly hydrate suspense when resolves to a Fragment without children', () => { + const originalHtml = ul([ + li(0), + li(1), + '', + '', + li(2), + li(3) + ]); + + const listeners = [sinon.spy(), sinon.spy(), sinon.spy(), sinon.spy()]; + + scratch.innerHTML = originalHtml; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + + + 0 + 1 + + + + + + 2 + 3 + + , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(getLog()).to.deep.equal([]); + expect(scratch.innerHTML).to.equal(originalHtml); + expect(listeners[3]).not.to.have.been.called; + + clearLog(); + scratch.querySelector('li:last-child').dispatchEvent(createEvent('click')); + expect(listeners[3]).to.have.been.calledOnce; + + return resolve(() => null).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal(originalHtml); + expect(getLog()).to.deep.equal([]); + clearLog(); + + scratch + .querySelector('li:nth-child(2)') + .dispatchEvent(createEvent('click')); + expect(listeners[1]).to.have.been.calledOnce; + + scratch + .querySelector('li:last-child') + .dispatchEvent(createEvent('click')); + expect(listeners[3]).to.have.been.calledTwice; + }); + }); + + it('Should hydrate a fragment with multiple children correctly', () => { + scratch.innerHTML = '

Hello
World!
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + + + , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '
Hello
World!
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(() => ( + <> +
Hello
+
World!
+ + )).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello
World!
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + + it('Should hydrate a fragment with no children correctly', () => { + scratch.innerHTML = '
Hello world
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + hydrate( + <> + + + +
Hello world
+ , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(() => null).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + + it('Should hydrate a fragment with no children correctly deeply', () => { + scratch.innerHTML = + '
Hello world
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + const [Lazy2, resolve2] = createLazy(); + hydrate( + <> + + + + + + + +
Hello world
+ , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(p => p.children).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + return resolve2(() => null).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + }); + + it('Should hydrate a fragment with multiple children correctly deeply', () => { + scratch.innerHTML = + '

I am

Fragment
Hello world
'; + clearLog(); + + const [Lazy, resolve] = createLazy(); + const [Lazy2, resolve2] = createLazy(); + hydrate( + <> + + + + + + + +
Hello world
+ , + scratch + ); + rerender(); // Flush rerender queue to mimic what preact will really do + expect(scratch.innerHTML).to.equal( + '

I am

Fragment
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + clearLog(); + + return resolve(p => p.children).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '

I am

Fragment
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + return resolve2(() => ( + <> +

I am

+ Fragment + + )).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal( + '

I am

Fragment
Hello world
' + ); + expect(getLog()).to.deep.equal([]); + + clearLog(); + }); + }); + }); }); diff --git a/src/diff/index.js b/src/diff/index.js index df7257dedd..b63af5f5d4 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -69,8 +69,8 @@ export function diff( // If the previous diff bailed out, resume creating/hydrating. if (oldVNode._flags & MODE_SUSPENDED) { isHydrating = !!(oldVNode._flags & MODE_HYDRATE); - oldDom = newVNode._dom = oldVNode._dom; - excessDomChildren = [oldDom]; + excessDomChildren = oldVNode._component._excess; + oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0]; } if ((tmp = options._diff)) tmp(newVNode); @@ -293,15 +293,56 @@ export function diff( // if hydrating or creating initial tree, bailout preserves DOM: if (isHydrating || excessDomChildren != NULL) { if (e.then) { + let shouldFallback = true, + commentMarkersToFind = 0, + done = false; + newVNode._flags |= isHydrating ? MODE_HYDRATE | MODE_SUSPENDED : MODE_SUSPENDED; - while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { - oldDom = oldDom.nextSibling; + newVNode._component._excess = []; + for (let i = 0; i < excessDomChildren.length; i++) { + let child = excessDomChildren[i]; + if (child == NULL || done) continue; + + // When we encounter a boundary with $s we are opening + // a boundary, this implies that we need to bump + // the amount of markers we need to find before closing + // the outer boundary. + // We exclude the open and closing marker from + // the future excessDomChildren but any nested one + // needs to be included for future suspensions. + if (child.nodeType == 8 && child.data == '$s') { + if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + } + commentMarkersToFind++; + shouldFallback = false; + excessDomChildren[i] = NULL; + } else if (child.nodeType == 8 && child.data == '/$s') { + commentMarkersToFind--; + if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + } + done = commentMarkersToFind === 0; + oldDom = excessDomChildren[i]; + excessDomChildren[i] = NULL; + } else if (commentMarkersToFind > 0) { + newVNode._component._excess.push(child); + excessDomChildren[i] = NULL; + } + } + + if (shouldFallback) { + while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { + oldDom = oldDom.nextSibling; + } + + excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL; + newVNode._component._excess.push(oldDom); } - excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL; newVNode._dom = oldDom; } else { for (let i = excessDomChildren.length; i--; ) { diff --git a/src/internal.d.ts b/src/internal.d.ts index 1c64329b4a..c32303ee0f 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -161,6 +161,7 @@ export interface Component

extends Omit, constructor: ComponentType

; state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks + _excess?: PreactElement[]; _dirty: boolean; _force?: boolean; _renderCallbacks: Array<() => void>; // Only class components From 1771d8772bd7b48ae3778d003b93012d253ceda2 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 13 Feb 2025 18:57:12 +0100 Subject: [PATCH 19/38] Move back to function --- src/diff/index.js | 12 +++++------- src/util.js | 8 ++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index b63af5f5d4..a5966e5268 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -13,7 +13,7 @@ import { BaseComponent, getDomSibling } from '../component'; import { Fragment } from '../create-element'; import { diffChildren } from './children'; import { setProperty } from './props'; -import { assign, isArray, slice } from '../util'; +import { assign, isArray, removeNode, slice } from '../util'; import options from '../options'; /** @@ -346,8 +346,7 @@ export function diff( newVNode._dom = oldDom; } else { for (let i = excessDomChildren.length; i--; ) { - const child = excessDomChildren[i]; - if (child) child.remove(); + removeNode(excessDomChildren[i]); } } } else { @@ -604,8 +603,7 @@ function diffElementNodes( // Remove children that are not part of any vnode. if (excessDomChildren != NULL) { for (i = excessDomChildren.length; i--; ) { - const child = excessDomChildren[i]; - if (child) child.remove(); + removeNode(excessDomChildren[i]); } } } @@ -704,8 +702,8 @@ export function unmount(vnode, parentVNode, skipRemove) { } } - if (!skipRemove && vnode._dom != null && vnode._dom.parentNode) { - vnode._dom.remove(); + if (!skipRemove) { + removeNode(vnode._dom); } vnode._component = vnode._parent = vnode._dom = UNDEFINED; diff --git a/src/util.js b/src/util.js index 282ab898d0..a73b6d9fa8 100644 --- a/src/util.js +++ b/src/util.js @@ -15,3 +15,11 @@ export function assign(obj, props) { for (let i in props) obj[i] = props[i]; return /** @type {O & P} */ (obj); } + +/** + * Remove a child node from its parent if attached. + * @param {import('./internal').PreactElement | null} node The node to remove + */ +export function removeNode(node) { + if (node && node.parentNode) node.remove(); +} From f0bc3e24faecb8b43c6e6220990a28dff219fb13 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 13 Feb 2025 18:58:03 +0100 Subject: [PATCH 20/38] Save bytes --- src/diff/index.js | 2 +- src/util.js | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index a5966e5268..cd17538819 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -249,7 +249,7 @@ export function diff( c.state = c._nextState; if (c.getChildContext != NULL) { - globalContext = assign(assign({}, globalContext), c.getChildContext()); + globalContext = assign({}, globalContext, c.getChildContext()); } if (isClassComponent && !isNew && c.getSnapshotBeforeUpdate != NULL) { diff --git a/src/util.js b/src/util.js index a73b6d9fa8..3f1d9bd2d6 100644 --- a/src/util.js +++ b/src/util.js @@ -2,19 +2,7 @@ import { EMPTY_ARR } from './constants'; export const isArray = Array.isArray; export const slice = EMPTY_ARR.slice; - -/** - * Assign properties from `props` to `obj` - * @template O, P The obj and props types - * @param {O} obj The object to copy properties to - * @param {P} props The object to copy properties from - * @returns {O & P} - */ -export function assign(obj, props) { - // @ts-expect-error We change the type of `obj` to be `O & P` - for (let i in props) obj[i] = props[i]; - return /** @type {O & P} */ (obj); -} +export const assign = Object.assign; /** * Remove a child node from its parent if attached. From b940a5fcf8ea3fbbc7b9577289b3a75bdf767484 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:07:38 -0600 Subject: [PATCH 21/38] refactor: Switch to Object.is for hook args (#4663) * Remove unused imports * refactor: Switch to Object.is for hook args * refactor: Copy to `useReducer` & store `Object` accessor * test: Add tests for `useEffect` & `useState` w/ `NaN` Co-authored-by: jayrobin --------- Co-authored-by: jdecroock Co-authored-by: jayrobin --- hooks/src/index.js | 9 ++++++--- hooks/test/browser/useEffect.test.js | 22 ++++++++++++++++++++++ hooks/test/browser/useState.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/hooks/src/index.js b/hooks/src/index.js index 1f40937fbd..f6c5130e84 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -1,5 +1,7 @@ import { options as _options } from 'preact'; +const ObjectIs = Object.is; + /** @type {number} */ let currentIndex; @@ -190,7 +192,7 @@ export function useReducer(reducer, initialState, init) { : hookState._value[0]; const nextValue = hookState._reducer(currentValue, action); - if (currentValue !== nextValue) { + if (!ObjectIs(currentValue, nextValue)) { hookState._nextValue = [nextValue, hookState._value[1]]; hookState._component.setState({}); } @@ -255,7 +257,8 @@ export function useReducer(reducer, initialState, init) { const currentValue = hookItem._value[0]; hookItem._value = hookItem._nextValue; hookItem._nextValue = undefined; - if (currentValue !== hookItem._value[0]) shouldUpdate = true; + if (!ObjectIs(currentValue, hookItem._value[0])) + shouldUpdate = true; } }); @@ -537,7 +540,7 @@ function argsChanged(oldArgs, newArgs) { return ( !oldArgs || oldArgs.length !== newArgs.length || - newArgs.some((arg, index) => arg !== oldArgs[index]) + newArgs.some((arg, index) => !ObjectIs(arg, oldArgs[index])) ); } diff --git a/hooks/test/browser/useEffect.test.js b/hooks/test/browser/useEffect.test.js index ce4bb73ab3..5bd0a910c7 100644 --- a/hooks/test/browser/useEffect.test.js +++ b/hooks/test/browser/useEffect.test.js @@ -636,4 +636,26 @@ describe('useEffect', () => { expect(calls.length).to.equal(1); expect(calls).to.deep.equal(['doing effecthi']); }); + + it('should not rerun when receiving NaN on subsequent renders', () => { + const calls = []; + const Component = ({ value }) => { + const [count, setCount] = useState(0); + useEffect(() => { + calls.push('doing effect' + count); + setCount(count + 1); + return () => { + calls.push('cleaning up' + count); + }; + }, [value]); + return

{count}

; + }; + const App = () => ; + + act(() => { + render(, scratch); + }); + expect(calls.length).to.equal(1); + expect(calls).to.deep.equal(['doing effect0']); + }); }); diff --git a/hooks/test/browser/useState.test.js b/hooks/test/browser/useState.test.js index ffa99e7902..c1693ff16f 100644 --- a/hooks/test/browser/useState.test.js +++ b/hooks/test/browser/useState.test.js @@ -372,6 +372,32 @@ describe('useState', () => { expect(scratch.innerHTML).to.equal('

hello world!!!

'); }); + it('should limit rerenders when setting state to NaN', () => { + const calls = []; + const App = ({ i }) => { + calls.push('rendering' + i); + const [greeting, setGreeting] = useState(0); + + if (i === 2) { + setGreeting(NaN); + } + + return

{greeting}

; + }; + + act(() => { + render(, scratch); + }); + expect(calls.length).to.equal(1); + expect(calls).to.deep.equal(['rendering1']); + + act(() => { + render(, scratch); + }); + expect(calls.length).to.equal(3); + expect(calls.slice(1).every(c => c === 'rendering2')).to.equal(true); + }); + describe('Global sCU', () => { let prevScu; before(() => { From f5d30d32aaf3d378e18c3df7b69daea0da77400f Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 14 Feb 2025 09:46:47 +0100 Subject: [PATCH 22/38] Golf hydration 2.0 --- src/diff/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index cd17538819..ba7144b143 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -293,8 +293,7 @@ export function diff( // if hydrating or creating initial tree, bailout preserves DOM: if (isHydrating || excessDomChildren != NULL) { if (e.then) { - let shouldFallback = true, - commentMarkersToFind = 0, + let commentMarkersToFind = 0, done = false; newVNode._flags |= isHydrating @@ -318,7 +317,6 @@ export function diff( newVNode._component._excess.push(child); } commentMarkersToFind++; - shouldFallback = false; excessDomChildren[i] = NULL; } else if (child.nodeType == 8 && child.data == '/$s') { commentMarkersToFind--; @@ -334,13 +332,13 @@ export function diff( } } - if (shouldFallback) { + if (!done) { while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) { oldDom = oldDom.nextSibling; } excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL; - newVNode._component._excess.push(oldDom); + newVNode._component._excess = [oldDom]; } newVNode._dom = oldDom; From f79fb18a17d45dbaef29f4d835de0d6fb4dc2c44 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 14 Feb 2025 09:57:42 +0100 Subject: [PATCH 23/38] Remove constant --- src/constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/constants.js b/src/constants.js index c60df07b93..2fe00830be 100644 --- a/src/constants.js +++ b/src/constants.js @@ -18,5 +18,3 @@ export const NULL = null; export const UNDEFINED = undefined; export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; -export const IS_NON_DIMENSIONAL = - /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; From 14b4026bbcaffebd04e45b288dbb8e5e5aa6b198 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 14 Feb 2025 09:59:01 +0100 Subject: [PATCH 24/38] Revert "Remove constant" This reverts commit 6b8bfa2f0a5c4580a9966ff5f66d7403d9ca1145. --- src/constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constants.js b/src/constants.js index 2fe00830be..c60df07b93 100644 --- a/src/constants.js +++ b/src/constants.js @@ -18,3 +18,5 @@ export const NULL = null; export const UNDEFINED = undefined; export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; +export const IS_NON_DIMENSIONAL = + /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; From fc17f946002fe2f49fb5c431900249614b889c46 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Fri, 14 Feb 2025 03:08:07 -0600 Subject: [PATCH 25/38] refactor: Breaking changes to outputs & pkg.json (#4652) * refactor: Switch to `package.json#exports.module`, drop `.min` builds, & use `.mjs` exclusively * chore: Remove leftover CJS shell * test: Fix export for karma * fix: coverage not generated in minify tests --------- Co-authored-by: Marvin Hagemeister --- compat/package.json | 4 ++-- config/node-13-exports.js | 32 -------------------------------- debug/package.json | 4 ++-- devtools/package.json | 4 ++-- hooks/package.json | 4 ++-- jsx-runtime/package.json | 4 ++-- karma.conf.js | 4 ++-- package.json | 21 ++++++++++----------- src/cjs.js | 3 --- test-utils/package.json | 4 ++-- 10 files changed, 24 insertions(+), 60 deletions(-) delete mode 100644 config/node-13-exports.js delete mode 100644 src/cjs.js diff --git a/compat/package.json b/compat/package.json index bb80a0fa7a..41decfd34b 100644 --- a/compat/package.json +++ b/compat/package.json @@ -5,7 +5,7 @@ "private": true, "description": "A React compatibility layer for Preact", "main": "dist/compat.js", - "module": "dist/compat.module.js", + "module": "dist/compat.mjs", "umd:main": "dist/compat.umd.js", "source": "src/index.js", "types": "src/index.d.ts", @@ -19,7 +19,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/compat.module.js", + "module": "./dist/compat.mjs", "umd": "./dist/compat.umd.js", "import": "./dist/compat.mjs", "require": "./dist/compat.js" diff --git a/config/node-13-exports.js b/config/node-13-exports.js deleted file mode 100644 index 9528d2aefa..0000000000 --- a/config/node-13-exports.js +++ /dev/null @@ -1,32 +0,0 @@ -const fs = require('fs'); - -const subRepositories = [ - 'compat', - 'debug', - 'devtools', - 'hooks', - 'jsx-runtime', - 'test-utils' -]; -const snakeCaseToCamelCase = str => - str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '')); - -const copyPreact = () => { - // Copy .module.js --> .mjs for Node 13 compat. - fs.writeFileSync( - `${process.cwd()}/dist/preact.mjs`, - fs.readFileSync(`${process.cwd()}/dist/preact.module.js`) - ); -}; - -const copy = name => { - // Copy .module.js --> .mjs for Node 13 compat. - const filename = name.includes('-') ? snakeCaseToCamelCase(name) : name; - fs.writeFileSync( - `${process.cwd()}/${name}/dist/${filename}.mjs`, - fs.readFileSync(`${process.cwd()}/${name}/dist/${filename}.module.js`) - ); -}; - -copyPreact(); -subRepositories.forEach(copy); diff --git a/debug/package.json b/debug/package.json index 836b4b49f7..df80213a7a 100644 --- a/debug/package.json +++ b/debug/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Preact extensions for development", "main": "dist/debug.js", - "module": "dist/debug.module.js", + "module": "dist/debug.mjs", "umd:main": "dist/debug.umd.js", "source": "src/index.js", "license": "MIT", @@ -18,7 +18,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/debug.module.js", + "module": "./dist/debug.mjs", "umd": "./dist/debug.umd.js", "import": "./dist/debug.mjs", "require": "./dist/debug.js" diff --git a/devtools/package.json b/devtools/package.json index c12ac730f0..353f2ad950 100644 --- a/devtools/package.json +++ b/devtools/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Preact bridge for Preact devtools", "main": "dist/devtools.js", - "module": "dist/devtools.module.js", + "module": "dist/devtools.mjs", "umd:main": "dist/devtools.umd.js", "source": "src/index.js", "license": "MIT", @@ -16,7 +16,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/devtools.module.js", + "module": "./dist/devtools.mjs", "umd": "./dist/devtools.umd.js", "import": "./dist/devtools.mjs", "require": "./dist/devtools.js" diff --git a/hooks/package.json b/hooks/package.json index 787927573e..ea32c3fe2f 100644 --- a/hooks/package.json +++ b/hooks/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Hook addon for Preact", "main": "dist/hooks.js", - "module": "dist/hooks.module.js", + "module": "dist/hooks.mjs", "umd:main": "dist/hooks.umd.js", "source": "src/index.js", "license": "MIT", @@ -26,7 +26,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/hooks.module.js", + "module": "./dist/hooks.mjs", "umd": "./dist/hooks.umd.js", "import": "./dist/hooks.mjs", "require": "./dist/hooks.js" diff --git a/jsx-runtime/package.json b/jsx-runtime/package.json index 1014de1c82..6a0cf9cd1c 100644 --- a/jsx-runtime/package.json +++ b/jsx-runtime/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Preact JSX runtime", "main": "dist/jsxRuntime.js", - "module": "dist/jsxRuntime.module.js", + "module": "dist/jsxRuntime.mjs", "umd:main": "dist/jsxRuntime.umd.js", "source": "src/index.js", "types": "src/index.d.ts", @@ -19,7 +19,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/jsxRuntime.module.js", + "module": "./dist/jsxRuntime.mjs", "umd": "./dist/jsxRuntime.umd.js", "import": "./dist/jsxRuntime.mjs", "require": "./dist/jsxRuntime.js" diff --git a/karma.conf.js b/karma.conf.js index 258acdb9a4..58cbe1d0a3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -62,7 +62,7 @@ const subPkgPath = pkgName => { const stripped = pkgName.replace(/[/\\./]/g, ''); const pkgJson = path.join(__dirname, 'package.json'); const pkgExports = require(pkgJson).exports; - const file = pkgExports[stripped ? `./${stripped}` : '.'].browser; + const file = pkgExports[stripped ? `./${stripped}` : '.'].module; return path.join(__dirname, file); }; @@ -154,7 +154,7 @@ function createEsbuildPlugin() { coverage && [ 'istanbul', { - include: minify ? '**/dist/**/*.js' : '**/src/**/*.js' + include: minify ? '**/dist/**/*.{js,mjs}' : '**/src/**/*.js' } ] ].filter(Boolean) diff --git a/package.json b/package.json index 41e58b0d8b..b1f8ba45a9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "description": "Fast 3kb React-compatible Virtual DOM library.", "main": "dist/preact.js", - "module": "dist/preact.module.js", + "module": "dist/preact.mjs", "umd:main": "dist/preact.umd.js", "unpkg": "dist/preact.min.js", "source": "src/index.js", @@ -20,56 +20,56 @@ "types": "./src/index-5.d.ts" }, "types": "./src/index.d.ts", - "browser": "./dist/preact.module.js", + "module": "./dist/preact.mjs", "umd": "./dist/preact.umd.js", "import": "./dist/preact.mjs", "require": "./dist/preact.js" }, "./compat": { "types": "./compat/src/index.d.ts", - "browser": "./compat/dist/compat.module.js", + "module": "./compat/dist/compat.mjs", "umd": "./compat/dist/compat.umd.js", "import": "./compat/dist/compat.mjs", "require": "./compat/dist/compat.js" }, "./debug": { "types": "./debug/src/index.d.ts", - "browser": "./debug/dist/debug.module.js", + "module": "./debug/dist/debug.mjs", "umd": "./debug/dist/debug.umd.js", "import": "./debug/dist/debug.mjs", "require": "./debug/dist/debug.js" }, "./devtools": { "types": "./devtools/src/index.d.ts", - "browser": "./devtools/dist/devtools.module.js", + "module": "./devtools/dist/devtools.mjs", "umd": "./devtools/dist/devtools.umd.js", "import": "./devtools/dist/devtools.mjs", "require": "./devtools/dist/devtools.js" }, "./hooks": { "types": "./hooks/src/index.d.ts", - "browser": "./hooks/dist/hooks.module.js", + "module": "./hooks/dist/hooks.mjs", "umd": "./hooks/dist/hooks.umd.js", "import": "./hooks/dist/hooks.mjs", "require": "./hooks/dist/hooks.js" }, "./test-utils": { "types": "./test-utils/src/index.d.ts", - "browser": "./test-utils/dist/testUtils.module.js", + "module": "./test-utils/dist/testUtils.mjs", "umd": "./test-utils/dist/testUtils.umd.js", "import": "./test-utils/dist/testUtils.mjs", "require": "./test-utils/dist/testUtils.js" }, "./jsx-runtime": { "types": "./jsx-runtime/src/index.d.ts", - "browser": "./jsx-runtime/dist/jsxRuntime.module.js", + "module": "./jsx-runtime/dist/jsxRuntime.mjs", "umd": "./jsx-runtime/dist/jsxRuntime.umd.js", "import": "./jsx-runtime/dist/jsxRuntime.mjs", "require": "./jsx-runtime/dist/jsxRuntime.js" }, "./jsx-dev-runtime": { "types": "./jsx-runtime/src/index.d.ts", - "browser": "./jsx-runtime/dist/jsxRuntime.module.js", + "module": "./jsx-runtime/dist/jsxRuntime.mjs", "umd": "./jsx-runtime/dist/jsxRuntime.umd.js", "import": "./jsx-runtime/dist/jsxRuntime.mjs", "require": "./jsx-runtime/dist/jsxRuntime.js" @@ -116,14 +116,13 @@ "prepare": "husky && run-s build", "build": "npm-run-all --parallel build:*", "build:core": "microbundle build --raw --no-generateTypes -f cjs,esm,umd", - "build:core-min": "microbundle build --raw --no-generateTypes -f cjs,esm,umd,iife src/cjs.js -o dist/preact.min.js", "build:debug": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd debug", "build:devtools": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd devtools", "build:hooks": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd hooks", "build:test-utils": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd test-utils", "build:compat": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd compat --globals 'preact/hooks=preactHooks'", "build:jsx": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd jsx-runtime", - "postbuild": "node ./config/node-13-exports.js && node ./config/compat-entries.js", + "postbuild": "node ./config/compat-entries.js", "dev": "microbundle watch --raw --no-generateTypes --format cjs", "dev:hooks": "microbundle watch --raw --no-generateTypes --format cjs --cwd hooks", "dev:compat": "microbundle watch --raw --no-generateTypes --format cjs --cwd compat --globals 'preact/hooks=preactHooks'", diff --git a/src/cjs.js b/src/cjs.js deleted file mode 100644 index b4721b1d44..0000000000 --- a/src/cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -import * as preact from './index.js'; -if (typeof module < 'u') module.exports = preact; -else self.preact = preact; diff --git a/test-utils/package.json b/test-utils/package.json index dc9f4ced19..1690edc2d8 100644 --- a/test-utils/package.json +++ b/test-utils/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Test-utils for Preact", "main": "dist/testUtils.js", - "module": "dist/testUtils.module.js", + "module": "dist/testUtils.mjs", "umd:main": "dist/testUtils.umd.js", "source": "src/index.js", "license": "MIT", @@ -19,7 +19,7 @@ "exports": { ".": { "types": "./src/index.d.ts", - "browser": "./dist/testUtils.module.js", + "module": "./dist/testUtils.mjs", "umd": "./dist/testUtils.umd.js", "import": "./dist/testUtils.mjs", "require": "./dist/testUtils.js" From cbc77b3dbe78c1dcffb8d311b9d70d77e6d98d63 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 10:09:13 +0100 Subject: [PATCH 26/38] Remove automatic px suffix (#4665) * Remove automatic px suffix * Remove from jsx-runtime --- compat/src/render.js | 14 +- compat/src/util.js | 3 + compat/test/browser/render.test.js | 205 +++++++++++++++++++ jsx-runtime/src/index.js | 27 +-- jsx-runtime/test/browser/jsx-runtime.test.js | 4 +- src/diff/props.js | 6 +- test/browser/style.test.js | 4 +- 7 files changed, 236 insertions(+), 27 deletions(-) diff --git a/compat/src/render.js b/compat/src/render.js index 18fb74319b..9807c0fc0b 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -24,7 +24,7 @@ import { useSyncExternalStore, useTransition } from './index'; -import { assign } from './util'; +import { assign, IS_NON_DIMENSIONAL } from './util'; export const REACT_ELEMENT_TYPE = Symbol.for('react.element'); @@ -117,7 +117,17 @@ function handleDomVNode(vnode) { } let lowerCased = i.toLowerCase(); - if (i === 'defaultValue' && 'value' in props && props.value == null) { + if (i === 'style' && typeof value === 'object') { + for (let key in value) { + if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) { + value[key] += 'px'; + } + } + } else if ( + i === 'defaultValue' && + 'value' in props && + props.value == null + ) { // `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined. // `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property. i = 'value'; diff --git a/compat/src/util.js b/compat/src/util.js index 5a5c11a363..55a7e52c44 100644 --- a/compat/src/util.js +++ b/compat/src/util.js @@ -11,3 +11,6 @@ export function shallowDiffers(a, b) { for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true; return false; } + +export const IS_NON_DIMENSIONAL = + /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; diff --git a/compat/test/browser/render.test.js b/compat/test/browser/render.test.js index 7781acb6d8..18a0972c5f 100644 --- a/compat/test/browser/render.test.js +++ b/compat/test/browser/render.test.js @@ -597,4 +597,209 @@ describe('compat render', () => { expect(scratch.textContent).to.equal('foo'); }); + + it('should append "px" to unitless inline css values', () => { + // These are all CSS Properties that support a single value + // that must have a unit. If we encounter a number we append "px" to it. + // The list is taken from: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference + const unitless = { + 'border-block': 2, + 'border-block-end-width': 3, + 'border-block-start-width': 4, + 'border-block-width': 5, + 'border-bottom-left-radius': 6, + 'border-bottom-right-radius': 7, + 'border-bottom-width': 8, + 'border-end-end-radius': 9, + 'border-end-start-radius': 10, + 'border-image-outset': 11, + 'border-image-width': 12, + 'border-inline': 2, + 'border-inline-end': 3, + 'border-inline-end-width': 4, + 'border-inline-start': 1, + 'border-inline-start-width': 123, + 'border-inline-width': 123, + 'border-left': 123, + 'border-left-width': 123, + 'border-radius': 123, + 'border-right': 123, + 'border-right-width': 123, + 'border-spacing': 123, + 'border-start-end-radius': 123, + 'border-start-start-radius': 123, + 'border-top': 123, + 'border-top-left-radius': 123, + 'border-top-right-radius': 123, + 'border-top-width': 123, + 'border-width': 123, + bottom: 123, + 'column-gap': 123, + 'column-rule-width': 23, + 'column-width': 23, + 'flex-basis': 23, + 'font-size': 123, + 'grid-gap': 23, + 'grid-auto-columns': 123, + 'grid-auto-rows': 123, + 'grid-template-columns': 23, + 'grid-template-rows': 23, + height: 123, + 'inline-size': 23, + inset: 23, + 'inset-block-end': 12, + 'inset-block-start': 12, + 'inset-inline-end': 213, + 'inset-inline-start': 213, + left: 213, + 'letter-spacing': 213, + margin: 213, + 'margin-block': 213, + 'margin-block-end': 213, + 'margin-block-start': 213, + 'margin-bottom': 213, + 'margin-inline': 213, + 'margin-inline-end': 213, + 'margin-inline-start': 213, + 'margin-left': 213, + 'margin-right': 213, + 'margin-top': 213, + 'mask-position': 23, + 'mask-size': 23, + 'max-block-size': 23, + 'max-height': 23, + 'max-inline-size': 23, + 'max-width': 23, + 'min-block-size': 23, + 'min-height': 23, + 'min-inline-size': 23, + 'min-width': 23, + 'object-position': 23, + 'outline-offset': 23, + 'outline-width': 123, + padding: 123, + 'padding-block': 123, + 'padding-block-end': 123, + 'padding-block-start': 123, + 'padding-bottom': 123, + 'padding-inline': 123, + 'padding-inline-end': 123, + 'padding-inline-start': 123, + 'padding-left': 123, + 'padding-right': 123, + 'padding-top': 123, + perspective: 123, + right: 123, + 'scroll-margin': 123, + 'scroll-margin-block': 123, + 'scroll-margin-block-start': 123, + 'scroll-margin-bottom': 123, + 'scroll-margin-inline': 123, + 'scroll-margin-inline-end': 123, + 'scroll-margin-inline-start': 123, + 'scroll-margin-inline-left': 123, + 'scroll-margin-inline-right': 123, + 'scroll-margin-inline-top': 123, + 'scroll-padding': 123, + 'scroll-padding-block': 123, + 'scroll-padding-block-end': 123, + 'scroll-padding-block-start': 123, + 'scroll-padding-bottom': 123, + 'scroll-padding-inline': 123, + 'scroll-padding-inline-end': 123, + 'scroll-padding-inline-start': 123, + 'scroll-padding-left': 123, + 'scroll-padding-right': 123, + 'scroll-padding-top': 123, + 'shape-margin': 123, + 'text-decoration-thickness': 123, + 'text-indent': 123, + 'text-underline-offset': 123, + top: 123, + 'transform-origin': 123, + translate: 123, + width: 123, + 'word-spacing': 123 + }; + + // These are all CSS properties that have valid numeric values. + // Our appending logic must not be applied here + const untouched = { + '-webkit-line-clamp': 2, + 'animation-iteration-count': 3, + 'column-count': 2, + // TODO: unsupported atm + // columns: 2, + flex: 1, + 'flex-grow': 1, + 'flex-shrink': 1, + 'font-size-adjust': 123, + 'font-weight': 12, + 'grid-column': 2, + 'grid-column-end': 2, + 'grid-column-start': 2, + 'grid-row': 2, + 'grid-row-end': 2, + 'grid-row-start': 2, + // TODO: unsupported atm + //'line-height': 2, + 'mask-border-outset': 2, + 'mask-border-slice': 2, + 'mask-border-width': 2, + 'max-zoom': 2, + 'min-zoom': 2, + opacity: 123, + order: 123, + orphans: 2, + 'grid-row-gap': 23, + scale: 23, + // TODO: unsupported atm + //'tab-size': 23, + widows: 123, + 'z-index': 123, + zoom: 123 + }; + + render( +
, + scratch + ); + + let style = scratch.firstChild.style; + + // Check properties that MUST not be changed + for (const key in unitless) { + // Check if css property is supported + if ( + window.CSS && + typeof window.CSS.supports === 'function' && + window.CSS.supports(key, unitless[key]) + ) { + expect( + String(style[key]).endsWith('px'), + `Should append px "${key}: ${unitless[key]}" === "${key}: ${style[key]}"` + ).to.equal(true); + } + } + + // Check properties that MUST not be changed + for (const key in untouched) { + // Check if css property is supported + if ( + window.CSS && + typeof window.CSS.supports === 'function' && + window.CSS.supports(key, untouched[key]) + ) { + expect( + !String(style[key]).endsWith('px'), + `Should be left as is: "${key}: ${untouched[key]}" === "${key}: ${style[key]}"` + ).to.equal(true); + } + } + }); }); diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index 0d05306ecd..8d7cbaa7f6 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -1,6 +1,5 @@ import { options, Fragment } from 'preact'; import { encodeEntities } from './utils'; -import { IS_NON_DIMENSIONAL } from '../../src/constants'; let vnodeId = 0; @@ -19,9 +18,9 @@ const isArray = Array.isArray; /** * JSX.Element factory used by Babel's {runtime:"automatic"} JSX transform - * @param {VNode['type']} type - * @param {VNode['props']} props - * @param {VNode['key']} [key] + * @param {import('../../src/internal').VNode['type']} type + * @param {import('preact').VNode['props']} props + * @param {import('preact').VNode['key']} [key] * @param {unknown} [isStaticChildren] * @param {unknown} [__source] * @param {unknown} [__self] @@ -46,7 +45,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { } } - /** @type {VNode & { __source: any; __self: any }} */ + /** @type {import('../../src/internal').VNode & { __source: any; __self: any }} */ const vnode = { type, props: normalizedProps, @@ -73,12 +72,13 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { * Create a template vnode. This function is not expected to be * used directly, but rather through a precompile JSX transform * @param {string[]} templates - * @param {Array} exprs - * @returns {VNode} + * @param {Array} exprs + * @returns {import('preact').VNode} */ function jsxTemplate(templates, ...exprs) { const vnode = createVNode(Fragment, { tpl: templates, exprs }); // Bypass render to string top level Fragment optimization + // @ts-ignore vnode.key = vnode._vnode; return vnode; } @@ -112,16 +112,7 @@ function jsxAttr(name, value) { : JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase()); - let suffix = ';'; - if ( - typeof val === 'number' && - // Exclude custom-attributes - !name.startsWith('--') && - !IS_NON_DIMENSIONAL.test(name) - ) { - suffix = 'px;'; - } - str = str + name + ':' + val + suffix; + str = str + name + ':' + val + ';'; } } return name + '="' + str + '"'; @@ -144,7 +135,7 @@ function jsxAttr(name, value) { * is not expected to be used directly, but rather through a * precompile JSX transform * @param {*} value - * @returns {string | null | VNode | Array} + * @returns {string | null | import('preact').VNode | Array} */ function jsxEscape(value) { if ( diff --git a/jsx-runtime/test/browser/jsx-runtime.test.js b/jsx-runtime/test/browser/jsx-runtime.test.js index 0c6a8fae72..32fed9762f 100644 --- a/jsx-runtime/test/browser/jsx-runtime.test.js +++ b/jsx-runtime/test/browser/jsx-runtime.test.js @@ -133,7 +133,9 @@ describe('precompiled JSX', () => { }); it('should serialize style object', () => { - expect(jsxAttr('style', { padding: 3 })).to.equal('style="padding:3px;"'); + expect(jsxAttr('style', { padding: '3px' })).to.equal( + 'style="padding:3px;"' + ); }); }); diff --git a/src/diff/props.js b/src/diff/props.js index a9d939382d..5f8e125ebb 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -1,4 +1,4 @@ -import { IS_NON_DIMENSIONAL, NULL, SVG_NAMESPACE } from '../constants'; +import { NULL, SVG_NAMESPACE } from '../constants'; import options from '../options'; function setStyle(style, key, value) { @@ -6,10 +6,8 @@ function setStyle(style, key, value) { style.setProperty(key, value == NULL ? '' : value); } else if (value == NULL) { style[key] = ''; - } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) { - style[key] = value; } else { - style[key] = value + 'px'; + style[key] = value; } } diff --git a/test/browser/style.test.js b/test/browser/style.test.js index e9a1ba91f7..e4baf0f99c 100644 --- a/test/browser/style.test.js +++ b/test/browser/style.test.js @@ -55,8 +55,8 @@ describe('style attribute', () => { backgroundPosition: '10px 10px', 'background-size': 'cover', gridRowStart: 1, - padding: 5, - top: 100, + padding: '5px', + top: '100px', left: '100%' }; From e942e4980f065cd444dc8d3f4f3e7f8dd241674f Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 12:57:33 +0100 Subject: [PATCH 27/38] Remove contains with a simple parentNode check (#4666) --- compat/src/portals.js | 1 - src/diff/children.js | 2 +- src/index-5.d.ts | 1 - src/index.d.ts | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/compat/src/portals.js b/compat/src/portals.js index c480a3d3a3..4cdeff370e 100644 --- a/compat/src/portals.js +++ b/compat/src/portals.js @@ -39,7 +39,6 @@ function Portal(props) { nodeType: 1, parentNode: container, childNodes: [], - contains: () => true, // Technically this isn't needed appendChild(child) { this.childNodes.push(child); diff --git a/src/diff/children.js b/src/diff/children.js index 25ebcd3660..7f401a0373 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -358,7 +358,7 @@ function insert(parentVNode, oldDom, parentDom) { return oldDom; } else if (parentVNode._dom != oldDom) { - if (oldDom && parentVNode.type && !parentDom.contains(oldDom)) { + if (oldDom && parentVNode.type && !oldDom.parentNode) { oldDom = getDomSibling(parentVNode); } parentDom.insertBefore(parentVNode._dom, oldDom || NULL); diff --git a/src/index-5.d.ts b/src/index-5.d.ts index 8bf29da0f6..be90714274 100644 --- a/src/index-5.d.ts +++ b/src/index-5.d.ts @@ -290,7 +290,6 @@ interface ContainerNode { readonly firstChild: ContainerNode | null; readonly childNodes: ArrayLike; - contains(other: ContainerNode | null): boolean; insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode; appendChild(node: ContainerNode): ContainerNode; removeChild(child: ContainerNode): ContainerNode; diff --git a/src/index.d.ts b/src/index.d.ts index 287d575c53..51321916f9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -289,7 +289,6 @@ interface ContainerNode { readonly firstChild: ContainerNode | null; readonly childNodes: ArrayLike; - contains(other: ContainerNode | null): boolean; insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode; appendChild(node: ContainerNode): ContainerNode; removeChild(child: ContainerNode): ContainerNode; From 577ddffc5b427bfd4d877aeb615db426d521586a Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 13:00:43 +0100 Subject: [PATCH 28/38] Prune portals (#4667) --- compat/src/portals.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/compat/src/portals.js b/compat/src/portals.js index 4cdeff370e..5ddb0129d7 100644 --- a/compat/src/portals.js +++ b/compat/src/portals.js @@ -39,18 +39,9 @@ function Portal(props) { nodeType: 1, parentNode: container, childNodes: [], - // Technically this isn't needed - appendChild(child) { - this.childNodes.push(child); - _this._container.appendChild(child); - }, insertBefore(child, before) { this.childNodes.push(child); _this._container.insertBefore(child, before); - }, - removeChild(child) { - this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1); - _this._container.removeChild(child); } }; } From 08fdd9fb09fd34406659eaa176042839fdf05e65 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 13:10:52 +0100 Subject: [PATCH 29/38] Remove SuspenseList mechanism (#4668) --- compat/src/suspense.js | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/compat/src/suspense.js b/compat/src/suspense.js index f47da4ec30..1e5bd55fc6 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -125,8 +125,6 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) { } c._suspenders.push(suspendingComponent); - const resolve = suspended(c._vnode); - let resolved = false; const onResolved = () => { if (resolved) return; @@ -134,11 +132,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) { resolved = true; suspendingComponent._onResolve = null; - if (resolve) { - resolve(onSuspensionComplete); - } else { - onSuspensionComplete(); - } + onSuspensionComplete(); }; suspendingComponent._onResolve = onResolved; @@ -218,29 +212,6 @@ Suspense.prototype.render = function (props, state) { ]; }; -/** - * Checks and calls the parent component's _suspended method, passing in the - * suspended vnode. This is a way for a parent to get notified - * that one of its children/descendants suspended. - * - * The parent MAY return a callback. The callback will get called when the - * suspension resolves, notifying the parent of the fact. - * Moreover, the callback gets function `unsuspend` as a parameter. The resolved - * child descendant will not actually get unsuspended until `unsuspend` gets called. - * This is a way for the parent to delay unsuspending. - * - * If the parent does not return a callback then the resolved vnode - * gets unsuspended immediately when it resolves. - * - * @param {import('./internal').VNode} vnode - * @returns {((unsuspend: () => void) => void)?} - */ -function suspended(vnode) { - /** @type {import('./internal').Component} */ - let component = vnode._parent._component; - return component && component._suspended && component._suspended(vnode); -} - export function lazy(loader) { let prom; let component; From 20ad273a87451db3bc3b319eb3efd6c81610dcc8 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 16:14:56 +0100 Subject: [PATCH 30/38] Remove static dom bail (#4670) --- src/diff/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index ba7144b143..66b67c3fdc 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -353,12 +353,6 @@ export function diff( } options._catchError(e, newVNode, oldVNode); } - } else if ( - excessDomChildren == NULL && - newVNode._original == oldVNode._original - ) { - newVNode._children = oldVNode._children; - newVNode._dom = oldVNode._dom; } else { oldDom = newVNode._dom = diffElementNodes( oldVNode._dom, From 23fee4481db7c368d88df7024a8d7570afa89b37 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 14 Feb 2025 20:05:22 +0100 Subject: [PATCH 31/38] Add mangle entry for _excess --- mangle.json | 1 + 1 file changed, 1 insertion(+) diff --git a/mangle.json b/mangle.json index 880aeafdaa..d999d593f9 100644 --- a/mangle.json +++ b/mangle.json @@ -42,6 +42,7 @@ "$_mask": "__m", "$_detachOnNextRender": "__b", "$_force": "__e", + "$_excess": "__z", "$_nextState": "__s", "$_renderCallbacks": "__h", "$_stateCallbacks": "_sb", From 5c818207efd30be2acf6fc609c71fe89922c070f Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 16 Feb 2025 09:57:16 +0100 Subject: [PATCH 32/38] Prove bailout still happens --- src/diff/index.js | 7 ++++-- test/browser/createContext.test.js | 35 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/diff/index.js b/src/diff/index.js index 66b67c3fdc..30eed07d62 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -69,8 +69,11 @@ export function diff( // If the previous diff bailed out, resume creating/hydrating. if (oldVNode._flags & MODE_SUSPENDED) { isHydrating = !!(oldVNode._flags & MODE_HYDRATE); - excessDomChildren = oldVNode._component._excess; - oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0]; + if (oldVNode._component._excess) { + excessDomChildren = oldVNode._component._excess; + oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0]; + oldVNode._component._excess = null; + } } if ((tmp = options._diff)) tmp(newVNode); diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js index b6de4113f9..a170d3fc2c 100644 --- a/test/browser/createContext.test.js +++ b/test/browser/createContext.test.js @@ -128,6 +128,41 @@ describe('createContext', () => { expect(renders).to.equal(1); }); + it('skips referentially equal children to Provider w/ dom-node in between', () => { + const { Provider, Consumer } = createContext(null); + let set, + renders = 0; + const Layout = ({ children }) => { + renders++; + return children; + }; + class State extends Component { + constructor(props) { + super(props); + this.state = { i: 0 }; + set = this.setState.bind(this); + } + render() { + const { children } = this.props; + return {children}; + } + } + const App = () => ( + +
+ + {({ i }) =>

{i}

}
+
+
+
+ ); + render(, scratch); + expect(renders).to.equal(1); + set({ i: 2 }); + rerender(); + expect(renders).to.equal(1); + }); + it('should preserve provider context through nesting providers', done => { const { Provider, Consumer } = createContext(null); const CONTEXT = { a: 'a' }; From 0d93b49b82a0468723d6e4b1e3210a392a2e1717 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:14:39 -0600 Subject: [PATCH 33/38] types: Require initial value in `useRef` (#4683) --- compat/src/index.d.ts | 19 +------------------ hooks/src/index.d.ts | 13 +++---------- src/index.d.ts | 4 ++-- 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/compat/src/index.d.ts b/compat/src/index.d.ts index 41f593c5a1..29bb5ba82d 100644 --- a/compat/src/index.d.ts +++ b/compat/src/index.d.ts @@ -5,16 +5,6 @@ import * as preact1 from 'preact'; import { JSXInternal } from '../../src/jsx'; import * as _Suspense from './suspense'; -<<<<<<< HEAD -======= -interface SignalLike { - value: T; - peek(): T; - subscribe(fn: (value: T) => void): () => void; -} - -type Signalish = T | SignalLike; - declare namespace preact { export interface FunctionComponent

{ ( @@ -108,7 +98,6 @@ declare namespace preact { } } ->>>>>>> Move `defaultProps` into `preact/compat` (#4657) // export default React; export = React; export as namespace React; @@ -120,7 +109,6 @@ declare namespace React { export import CreateHandle = _hooks.CreateHandle; export import EffectCallback = _hooks.EffectCallback; export import Inputs = _hooks.Inputs; - export import PropRef = _hooks.PropRef; export import Reducer = _hooks.Reducer; export import Dispatch = _hooks.Dispatch; export import SetStateAction = _hooks.StateUpdater; @@ -356,7 +344,7 @@ declare namespace React { } export interface ForwardRefRenderFunction { - (props: P, ref: ForwardedRef): preact.ComponentChild; + (props: P, ref: ForwardedRef): preact1.ComponentChild; displayName?: string; } @@ -366,13 +354,8 @@ declare namespace React { } export function forwardRef( -<<<<<<< HEAD fn: ForwardRefRenderFunction - ): preact.FunctionalComponent & { ref?: preact.Ref }>; -======= - fn: ForwardFn ): preact1.FunctionalComponent & { ref?: preact1.Ref }>; ->>>>>>> Move `defaultProps` into `preact/compat` (#4657) export type PropsWithoutRef

= Omit; diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts index d7f77dbb49..22147a2bd4 100644 --- a/hooks/src/index.d.ts +++ b/hooks/src/index.d.ts @@ -52,13 +52,6 @@ export function useReducer( init: (arg: I) => S ): [S, Dispatch]; -/** @deprecated Use the `Ref` type instead. */ -type PropRef = MutableRef; - -interface MutableRef { - current: T; -} - /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. @@ -68,9 +61,9 @@ interface MutableRef { * * @param initialValue the initial value to store in the ref object */ -export function useRef(initialValue: T): MutableRef; -export function useRef(initialValue: T | null): RefObject; -export function useRef(): MutableRef; +export function useRef(initialValue: T): RefObject; +export function useRef(initialValue: T | null): RefObject; +export function useRef(initialValue: T | undefined): RefObject; type EffectCallback = () => void | (() => void); /** diff --git a/src/index.d.ts b/src/index.d.ts index 51321916f9..e6b8fd549e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -37,9 +37,9 @@ export interface VNode

{ export type Key = string | number | any; -export type RefObject = { current: T | null }; +export type RefObject = { current: T }; export type RefCallback = (instance: T | null) => void; -export type Ref = RefObject | RefCallback | null; +export type Ref = RefCallback | RefObject | null; export type ComponentChild = | VNode From f52a120824d2abbc8472f9594b7eb0b6f9eac467 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Wed, 26 Feb 2025 19:11:02 +0100 Subject: [PATCH 34/38] Cleanup IE11 mentions --- README.md | 2 +- compat/test/browser/events.test.js | 13 +--- compat/test/browser/render.test.js | 7 +- compat/test/browser/textarea.test.js | 21 +----- test/browser/hydrate.test.js | 10 +-- test/browser/render.test.js | 100 ++++++++++----------------- test/polyfills.js | 6 -- 7 files changed, 47 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 65e9e0ddc8..85534123b5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - Extensive React compatibility via a simple [preact/compat] alias - Everything you need: JSX, VDOM, [DevTools], HMR, SSR. - Highly optimized diff algorithm and seamless hydration from Server Side Rendering -- Supports all modern browsers and IE11 +- Supports all modern browsers - Transparent asynchronous rendering with a pluggable scheduler ### 💁 More information at the [Preact Website ➞](https://preactjs.com) diff --git a/compat/test/browser/events.test.js b/compat/test/browser/events.test.js index 55c5bd8d63..589dd46f59 100644 --- a/compat/test/browser/events.test.js +++ b/compat/test/browser/events.test.js @@ -30,10 +30,6 @@ describe('preact/compat events', () => { it('should patch events', () => { let spy = sinon.spy(event => { - // Calling ev.preventDefault() outside of an event handler - // does nothing in IE11. So we move these asserts inside - // the event handler. We ensure that it's called once - // in another assertion expect(event.isDefaultPrevented()).to.be.false; event.preventDefault(); expect(event.isDefaultPrevented()).to.be.true; @@ -89,16 +85,11 @@ describe('preact/compat events', () => { expect(vnode.props).to.not.haveOwnProperty('oninputCapture'); }); - it('should normalize onChange for range, except in IE11', () => { - // NOTE: we don't normalize `onchange` for range inputs in IE11. - const eventType = /Trident\//.test(navigator.userAgent) - ? 'change' - : 'input'; - + it('should normalize onChange for range', () => { render( null} />, scratch); expect(proto.addEventListener).to.have.been.calledOnce; expect(proto.addEventListener).to.have.been.calledWithExactly( - eventType, + 'input', sinon.match.func, false ); diff --git a/compat/test/browser/render.test.js b/compat/test/browser/render.test.js index 18a0972c5f..2e4d0a1ac3 100644 --- a/compat/test/browser/render.test.js +++ b/compat/test/browser/render.test.js @@ -264,12 +264,7 @@ describe('compat render', () => { scratch ); - let html = sortAttributes(scratch.innerHTML); - if (/Trident/.test(navigator.userAgent)) { - html = html.toLowerCase(); - } - - expect(html).to.equal( + expect(sortAttributes(scratch.innerHTML)).to.equal( '' ); }); diff --git a/compat/test/browser/textarea.test.js b/compat/test/browser/textarea.test.js index ffc4078f07..f1628d3630 100644 --- a/compat/test/browser/textarea.test.js +++ b/compat/test/browser/textarea.test.js @@ -33,17 +33,10 @@ describe('Textarea', () => { hydrate(, scratch); expect(scratch.firstElementChild.value).to.equal('foo'); - // IE11 always displays the value as node.innerHTML - if (!/Trident/.test(window.navigator.userAgent)) { - expect(scratch.innerHTML).to.be.equal(''); - } + expect(scratch.innerHTML).to.be.equal(''); }); it('should alias defaultValue to children', () => { - // TODO: IE11 doesn't update `node.value` when - // `node.defaultValue` is set. - if (/Trident/.test(navigator.userAgent)) return; - render('); - } + expect(scratch.innerHTML).to.equal(''); expect(scratch.firstElementChild.value).to.equal('hello'); act(() => { set(''); }); - // Same as earlier: IE11 always displays the value as node.innerHTML - if (!/Trident/.test(window.navigator.userAgent)) { - expect(scratch.innerHTML).to.equal(''); - } + expect(scratch.innerHTML).to.equal(''); expect(scratch.firstElementChild.value).to.equal(''); }); }); diff --git a/test/browser/hydrate.test.js b/test/browser/hydrate.test.js index d97e2e0655..c176793629 100644 --- a/test/browser/hydrate.test.js +++ b/test/browser/hydrate.test.js @@ -225,10 +225,7 @@ describe('hydrate()', () => { clearLog(); hydrate(vnode, scratch); - // IE11 doesn't support spying on Element.prototype - if (!/Trident/.test(navigator.userAgent)) { - expect(attributesSpy.get).to.not.have.been.called; - } + expect(attributesSpy.get).to.not.have.been.called; expect(serializeHtml(scratch)).to.equal( sortAttributes( @@ -377,10 +374,7 @@ describe('hydrate()', () => { ); hydrate(preactElement, scratch); - // IE11 doesn't support spies on built-in prototypes - if (!/Trident/.test(navigator.userAgent)) { - expect(attributesSpy.get).to.not.have.been.called; - } + expect(attributesSpy.get).to.not.have.been.called; expect(scratch).to.have.property( 'innerHTML', '

' diff --git a/test/browser/render.test.js b/test/browser/render.test.js index 4d5562663c..e320400224 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -24,8 +24,6 @@ function getAttributes(node) { return attrs; } -const isIE11 = /Trident\//.test(navigator.userAgent); - describe('render()', () => { let scratch, rerender; @@ -84,13 +82,10 @@ describe('render()', () => { expect(scratch.innerHTML).to.eql(``); }); - // IE11 doesn't support these. - if (!/Trident/.test(window.navigator.userAgent)) { - it('should render px width and height on img correctly', () => { - render(, scratch); - expect(scratch.innerHTML).to.eql(``); - }); - } + it('should render px width and height on img correctly', () => { + render(, scratch); + expect(scratch.innerHTML).to.eql(``); + }); it('should support the