diff --git a/compat/src/index.d.ts b/compat/src/index.d.ts index a77c412bde..bc066c39df 100644 --- a/compat/src/index.d.ts +++ b/compat/src/index.d.ts @@ -2,7 +2,6 @@ import * as _hooks from '../../hooks'; import * as preact from '../../src'; import { JSXInternal } from '../../src/jsx'; import * as _Suspense from './suspense'; -import * as _SuspenseList from './suspense-list'; interface SignalLike { value: T; @@ -67,11 +66,10 @@ declare namespace React { export import ReactElement = preact.VNode; export import Consumer = preact.Consumer; export import ErrorInfo = preact.ErrorInfo; - + // Suspense export import Suspense = _Suspense.Suspense; export import lazy = _Suspense.lazy; - export import SuspenseList = _SuspenseList.SuspenseList; // Compat export import StrictMode = preact.Fragment; diff --git a/compat/src/index.js b/compat/src/index.js index 7c8da05506..650a18f147 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -25,7 +25,6 @@ import { memo } from './memo'; import { forwardRef } from './forwardRef'; import { Children } from './Children'; import { Suspense, lazy } from './suspense'; -import { SuspenseList } from './suspense-list'; import { createPortal } from './portals'; import { is } from './util'; import { @@ -241,7 +240,6 @@ export { unstable_batchedUpdates, StrictMode, Suspense, - SuspenseList, lazy, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED }; @@ -289,7 +287,6 @@ export default { unstable_batchedUpdates, StrictMode, Suspense, - SuspenseList, lazy, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED }; diff --git a/compat/src/suspense-list.d.ts b/compat/src/suspense-list.d.ts deleted file mode 100644 index caa1eb6bff..0000000000 --- a/compat/src/suspense-list.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, ComponentChild, ComponentChildren } from '../../src'; - -// -// SuspenseList -// ----------------------------------- - -export interface SuspenseListProps { - children?: ComponentChildren; - revealOrder?: 'forwards' | 'backwards' | 'together'; -} - -export class SuspenseList extends Component { - render(): ComponentChild; -} diff --git a/compat/src/suspense-list.js b/compat/src/suspense-list.js deleted file mode 100644 index 5e5d750a08..0000000000 --- a/compat/src/suspense-list.js +++ /dev/null @@ -1,127 +0,0 @@ -import { Component, toChildArray } from 'preact'; -import { suspended } from './suspense.js'; - -// Indexes to linked list nodes (nodes are stored as arrays to save bytes). -const SUSPENDED_COUNT = 0; -const RESOLVED_COUNT = 1; -const NEXT_NODE = 2; - -// Having custom inheritance instead of a class here saves a lot of bytes. -export function SuspenseList() { - this._next = null; - this._map = null; -} - -// Mark one of child's earlier suspensions as resolved. -// Some pending callbacks may become callable due to this -// (e.g. the last suspended descendant gets resolved when -// revealOrder === 'together'). Process those callbacks as well. -const resolve = (list, child, node) => { - if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) { - // The number a child (or any of its descendants) has been suspended - // matches the number of times it's been resolved. Therefore we - // mark the child as completely resolved by deleting it from ._map. - // This is used to figure out when *all* children have been completely - // resolved when revealOrder is 'together'. - list._map.delete(child); - } - - // If revealOrder is falsy then we can do an early exit, as the - // callbacks won't get queued in the node anyway. - // If revealOrder is 'together' then also do an early exit - // if all suspended descendants have not yet been resolved. - if ( - !list.props.revealOrder || - (list.props.revealOrder[0] === 't' && list._map.size) - ) { - return; - } - - // Walk the currently suspended children in order, calling their - // stored callbacks on the way. Stop if we encounter a child that - // has not been completely resolved yet. - node = list._next; - while (node) { - while (node.length > 3) { - node.pop()(); - } - if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) { - break; - } - list._next = node = node[NEXT_NODE]; - } -}; - -// Things we do here to save some bytes but are not proper JS inheritance: -// - call `new Component()` as the prototype -// - do not set `Suspense.prototype.constructor` to `Suspense` -SuspenseList.prototype = new Component(); - -SuspenseList.prototype._suspended = function (child) { - const list = this; - const delegated = suspended(list._vnode); - - let node = list._map.get(child); - node[SUSPENDED_COUNT]++; - - return unsuspend => { - const wrappedUnsuspend = () => { - if (!list.props.revealOrder) { - // Special case the undefined (falsy) revealOrder, as there - // is no need to coordinate a specific order or unsuspends. - unsuspend(); - } else { - node.push(unsuspend); - resolve(list, child, node); - } - }; - if (delegated) { - delegated(wrappedUnsuspend); - } else { - wrappedUnsuspend(); - } - }; -}; - -SuspenseList.prototype.render = function (props) { - this._next = null; - this._map = new Map(); - - const children = toChildArray(props.children); - if (props.revealOrder && props.revealOrder[0] === 'b') { - // If order === 'backwards' (or, well, anything starting with a 'b') - // then flip the child list around so that the last child will be - // the first in the linked list. - children.reverse(); - } - // Build the linked list. Iterate through the children in reverse order - // so that `_next` points to the first linked list node to be resolved. - for (let i = children.length; i--; ) { - // Create a new linked list node as an array of form: - // [suspended_count, resolved_count, next_node] - // where suspended_count and resolved_count are numeric counters for - // keeping track how many times a node has been suspended and resolved. - // - // Note that suspended_count starts from 1 instead of 0, so we can block - // processing callbacks until componentDidMount has been called. In a sense - // node is suspended at least until componentDidMount gets called! - // - // Pending callbacks are added to the end of the node: - // [suspended_count, resolved_count, next_node, callback_0, callback_1, ...] - this._map.set(children[i], (this._next = [1, 0, this._next])); - } - return props.children; -}; - -SuspenseList.prototype.componentDidUpdate = - SuspenseList.prototype.componentDidMount = function () { - // Iterate through all children after mounting for two reasons: - // 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases - // each node[RELEASED_COUNT] by 1, therefore balancing the counters. - // The nodes can now be completely consumed from the linked list. - // 2. Handle nodes that might have gotten resolved between render and - // componentDidMount. - this._map.forEach((node, child) => { - resolve(this, child, node); - }); - }; diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 32cc3dfd75..fad7dfa433 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -217,7 +217,7 @@ 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 (e.g. SuspenseList) to get notified + * 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 @@ -232,7 +232,7 @@ Suspense.prototype.render = function (props, state) { * @param {import('./internal').VNode} vnode * @returns {((unsuspend: () => void) => void)?} */ -export function suspended(vnode) { +function suspended(vnode) { /** @type {import('./internal').Component} */ let component = vnode._parent._component; return component && component._suspended && component._suspended(vnode); diff --git a/compat/test/browser/suspense-list.test.js b/compat/test/browser/suspense-list.test.js deleted file mode 100644 index f3d4c91c84..0000000000 --- a/compat/test/browser/suspense-list.test.js +++ /dev/null @@ -1,588 +0,0 @@ -import { setupRerender } from 'preact/test-utils'; -import React, { - createElement, - render, - Component, - Suspense, - SuspenseList -} from 'preact/compat'; -import { useState } from 'preact/hooks'; -import { setupScratch, teardown } from '../../../test/_util/helpers'; - -const h = React.createElement; -/* eslint-env browser, mocha */ - -function getSuspendableComponent(text) { - let resolve; - let resolved = false; - const promise = new Promise(_resolve => { - resolve = () => { - resolved = true; - _resolve(); - return promise; - }; - }); - - class LifecycleSuspender extends Component { - render() { - if (!resolved) { - throw promise; - } - return {text}; - } - } - - LifecycleSuspender.resolve = () => { - resolve(); - }; - - return LifecycleSuspender; -} - -describe('suspense-list', () => { - /** @type {HTMLDivElement} */ - let scratch, - rerender, - unhandledEvents = []; - - function onUnhandledRejection(event) { - unhandledEvents.push(event); - } - - function getSuspenseList(revealOrder) { - const A = getSuspendableComponent('A'); - const B = getSuspendableComponent('B'); - const C = getSuspendableComponent('C'); - render( - - Loading...}> - - - Loading...}> - - - Loading...}> - - - , - scratch - ); // Render initial state - - return [A.resolve, B.resolve, C.resolve]; - } - - function getNestedSuspenseList(outerRevealOrder, innerRevealOrder) { - const A = getSuspendableComponent('A'); - const B = getSuspendableComponent('B'); - const C = getSuspendableComponent('C'); - const D = getSuspendableComponent('D'); - - render( - - Loading...}> - - - - Loading...}> - - - Loading...}> - - - - Loading...}> - - - , - scratch - ); - return [A.resolve, B.resolve, C.resolve, D.resolve]; - } - - beforeEach(() => { - scratch = setupScratch(); - rerender = setupRerender(); - unhandledEvents = []; - - if ('onunhandledrejection' in window) { - window.addEventListener('unhandledrejection', onUnhandledRejection); - } - }); - - afterEach(() => { - teardown(scratch); - - if ('onunhandledrejection' in window) { - window.removeEventListener('unhandledrejection', onUnhandledRejection); - - if (unhandledEvents.length) { - throw unhandledEvents[0].reason; - } - } - }); - - it('should work for single element', async () => { - const Component = getSuspendableComponent('A'); - render( - - Loading...}> - - - , - scratch - ); // Render initial state - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql(`Loading...`); - - await Component.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql(`A`); - }); - - it('should let components appear backwards if no revealOrder is mentioned', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList(); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...BLoading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...BC` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should let components appear forwards if no revealOrder is mentioned', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList(); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ALoading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABLoading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should let components appear in forwards if revealOrder=forwards and first one resolves before others', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('forwards'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ALoading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ALoading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should make components appear together if revealOrder=forwards and others resolves before first', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('forwards'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should let components appear backwards if revealOrder=backwards and others resolves before first', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('backwards'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...C` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...BC` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should make components appear together if revealOrder=backwards and first one resolves others', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('backwards'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...C` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should make components appear together if revealOrder=together and first one resolves others', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('together'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should make components appear together if revealOrder=together and second one resolves before others', async () => { - const [resolver1, resolver2, resolver3] = getSuspenseList('together'); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver2(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver1(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...` - ); - - await resolver3(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABC` - ); - }); - - it('should not do anything to non suspense elements', async () => { - const A = getSuspendableComponent('A'); - const B = getSuspendableComponent('B'); - render( - - Loading...}> - - -
foo
- Loading...}> - - - bar - , - scratch - ); - - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...
foo
Loading...bar` - ); - - await A.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql( - `A
foo
Loading...bar` - ); - - await B.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql( - `A
foo
Bbar` - ); - }); - - it('should make sure nested SuspenseList works with forwards', async () => { - const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList( - 'forwards', - 'forwards' - ); - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveB(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveA(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABLoading...Loading...` - ); - - await resolveC(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABCLoading...` - ); - - await resolveD(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABCD` - ); - }); - - it('should make sure nested SuspenseList works with backwards', async () => { - const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList( - 'forwards', - 'backwards' - ); - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveA(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ALoading...Loading...Loading...` - ); - - await resolveC(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ALoading...CLoading...` - ); - - await resolveB(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABCLoading...` - ); - - await resolveD(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABCD` - ); - }); - - it('should make sure nested SuspenseList works with together', async () => { - const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList( - 'together', - 'forwards' - ); - rerender(); // Re-render with fallback cuz lazy threw - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveA(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveD(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveB(); - rerender(); - expect(scratch.innerHTML).to.eql( - `Loading...Loading...Loading...Loading...` - ); - - await resolveC(); - rerender(); - expect(scratch.innerHTML).to.eql( - `ABCD` - ); - }); - - it('should work with forwards even when a child does not suspend', async () => { - const Component = getSuspendableComponent('A'); - - render( - - Loading...}> -
- - Loading...}> - - - , - scratch - ); // Render initial state - - rerender(); - expect(scratch.innerHTML).to.eql(`
Loading...`); - - await Component.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql(`
A`); - }); - - it('should work with together even when a child does not suspend', async () => { - const Component = getSuspendableComponent('A'); - - render( - - Loading...}> -
- - Loading...}> - - - , - scratch - ); // Render initial state - - rerender(); - expect(scratch.innerHTML).to.eql(`
Loading...`); - - await Component.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql(`
A`); - }); - - it('should not suspend resolved children if a new suspense comes in between', async () => { - const ComponentA = getSuspendableComponent('A'); - const ComponentB = getSuspendableComponent('B'); - - let showB; - function Container() { - const [showHidden, setShowHidden] = useState(false); - showB = setShowHidden; - return ( - - Loading...}> -
- - {showHidden && ( - Loading...}> - - - )} - Loading...}> - - - - ); - } - render(, scratch); // Render initial state - - rerender(); - expect(scratch.innerHTML).to.eql(`
Loading...`); - - await ComponentA.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql(`
A`); - - showB(true); - rerender(); - expect(scratch.innerHTML).to.eql( - `
Loading...A` - ); - - await ComponentB.resolve(); - rerender(); - expect(scratch.innerHTML).to.eql(`
BA`); - }); -}); diff --git a/compat/test/ts/suspense.tsx b/compat/test/ts/suspense.tsx index c082f54663..3b6268f8a2 100644 --- a/compat/test/ts/suspense.tsx +++ b/compat/test/ts/suspense.tsx @@ -37,20 +37,6 @@ class ReactSuspensefulFunc extends React.Component { } } -//SuspenseList using lazy components -function ReactSuspenseListTester(_props: any) { - return ( - - }> - - - }> - - - - ); -} - const Comp = () =>

Hello world

; const importComponent = async () => {