From 28888a34152a2081f46ec6fb9ad1faa3fcce60d9 Mon Sep 17 00:00:00 2001 From: Nils Kuhnhenn Date: Sat, 31 Aug 2024 05:54:18 +0200 Subject: [PATCH] allow configuring custom fiber, add missing arguments --- packages/react/index.d.ts | 22 +++++-- packages/react/src/exports.js | 4 +- packages/react/src/reconciler/index.js | 1 + packages/react/src/render/index.js | 29 +++++++--- packages/react/src/stage/index.js | 27 +++++++-- .../test/__snapshots__/index.spec.js.snap | 57 +++++++++++++++++++ .../test/__snapshots__/stage.spec.js.snap | 38 +++++++++++++ packages/react/test/render.spec.js | 3 - packages/react/test/stage.spec.js | 19 ++++++- 9 files changed, 177 insertions(+), 23 deletions(-) diff --git a/packages/react/index.d.ts b/packages/react/index.d.ts index ca93eda8..6afad9ba 100644 --- a/packages/react/index.d.ts +++ b/packages/react/index.d.ts @@ -71,8 +71,11 @@ interface ReconcilerConfig mountEventComponent(): void; updateEventComponent(): void; handleEventTarget(): void; + noTimeout: any; scheduleTimeout(...args: any[]): any; cancelTimeout(...args: any[]): any; + supportsMicrotasks: boolean, + scheduleMicrotask(...args: any[]): any, appendChild(...args: any[]): any; appendChildToContainer(...args: any[]): any; removeChild(...args: any[]): any; @@ -277,6 +280,11 @@ declare namespace _ReactPixi */ raf?: boolean; + /** + * Use a custom reconciler. + */ + fiber?: Reconciler; + /** * Render the PIXI stage on React component changes. * You'll need to set raf={false}. @@ -391,7 +399,7 @@ export interface ReactPixiRoot { unmount(): void } -export const createRoot: (container: PixiContainer) => ReactPixiRoot +export const createRoot: (container: PixiContainer, fiber?: Reconciler) => ReactPixiRoot // renderer export const render: ( @@ -401,7 +409,7 @@ export const render: ( ) => any; // unmount component -export const unmountComponentAtNode: (container: PixiContainer) => void; +export const unmountComponentAtNode: (container: PixiContainer, fiber?: Reconciler) => void; // context export const AppContext: React.Context; @@ -409,9 +417,13 @@ export const AppProvider: React.ComponentType>; // fiber -export const PixiFiber: ( - eventsMap?: { [P in keyof ReconcilerConfig]: (...args: any) => void } -) => Reconciler; +export const PixiFiber: Reconciler; + +// re-exported reconciler from react +export const Reconciler: (config: ReconcilerConfig) => Reconciler; + +// default hostconfig +export const hostconfig: ReconcilerConfig; // stage export class Stage extends React.Component<_ReactPixi.IStage> {} diff --git a/packages/react/src/exports.js b/packages/react/src/exports.js index f2dabb7f..fa9afa86 100644 --- a/packages/react/src/exports.js +++ b/packages/react/src/exports.js @@ -1,7 +1,7 @@ import { PixiComponent, TYPES } from './utils/element'; import { createRoot, render, unmountComponentAtNode } from './render'; import Stage from './stage'; -import { PixiFiber } from './reconciler'; +import { PixiFiber, Reconciler, hostconfig } from './reconciler'; import { Context as AppContext, AppProvider, AppConsumer, withPixiApp } from './stage/provider'; import { useTick, useApp } from './hooks'; import { withFilters } from './hoc'; @@ -22,6 +22,8 @@ export { withPixiApp, PixiComponent, PixiFiber, + hostconfig, + Reconciler, AppProvider, AppConsumer, AppContext, diff --git a/packages/react/src/reconciler/index.js b/packages/react/src/reconciler/index.js index 94620ace..26938edd 100644 --- a/packages/react/src/reconciler/index.js +++ b/packages/react/src/reconciler/index.js @@ -2,6 +2,7 @@ import Reconciler from 'react-reconciler'; import pkg from '../../../../package.json'; import hostconfig from './hostconfig'; +export { hostconfig, Reconciler }; export const PixiFiber = Reconciler(hostconfig); export const VERSION = pkg.version; diff --git a/packages/react/src/render/index.js b/packages/react/src/render/index.js index 06f6dd5f..6690be25 100644 --- a/packages/react/src/render/index.js +++ b/packages/react/src/render/index.js @@ -1,6 +1,7 @@ import { Container } from '@pixi/display'; import invariant from '../utils/invariant'; import { PixiFiber } from '../reconciler'; +import { Reconciler } from 'react-reconciler'; // cache both root PixiFiber containers and React roots export const roots = new Map(); @@ -8,7 +9,7 @@ export const roots = new Map(); /** * @param {Container} container */ -function unmountComponent(container) +function unmountComponent(container, fiber = PixiFiber) { invariant( Container.prototype.isPrototypeOf(container), @@ -20,7 +21,7 @@ function unmountComponent(container) const { pixiFiberContainer } = roots.get(container); // unmount component - PixiFiber.updateContainer(null, pixiFiberContainer, undefined, () => + fiber.updateContainer(null, pixiFiberContainer, undefined, () => { roots.delete(container); }); @@ -32,9 +33,10 @@ function unmountComponent(container) * Use this without React-DOM * * @param {Container} container + * @param {Reconciler} custom fiber * @returns {{ render: Function, unmount: Function}} */ -export function createRoot(container) +export function createRoot(container, fiber = PixiFiber) { invariant( Container.prototype.isPrototypeOf(container), @@ -47,23 +49,32 @@ export function createRoot(container) if (!root) { - const pixiFiberContainer = PixiFiber.createContainer(container); + const pixiFiberContainer = fiber.createContainer( + container, + 0, + null, + false, + null, + '', + (error) => console.error('React recoverable error:', error), + null + ); const reactRoot = { render(element) { // schedules a top level update - PixiFiber.updateContainer( + fiber.updateContainer( element, pixiFiberContainer, undefined ); - return PixiFiber.getPublicRootInstance(pixiFiberContainer); + return fiber.getPublicRootInstance(pixiFiberContainer); }, unmount() { - unmountComponent(container); + unmountComponent(container, fiber); roots.delete(container); }, }; @@ -116,7 +127,7 @@ export function render(element, container, callback) * @deprecated use root.unmount() instead * @param {Container} container */ -export function unmountComponentAtNode(container) +export function unmountComponentAtNode(container, fiber) { - unmountComponent(container); + unmountComponent(container, fiber); } diff --git a/packages/react/src/stage/index.js b/packages/react/src/stage/index.js index e1e4304e..57d54d4e 100644 --- a/packages/react/src/stage/index.js +++ b/packages/react/src/stage/index.js @@ -46,6 +46,8 @@ const propTypes = { children: PropTypes.node, + fiber: PropTypes.any, + // PIXI options, see https://pixijs.download/v7.x/docs/PIXI.Application.html options: PropTypes.shape({ autoStart: PropTypes.bool, @@ -97,6 +99,7 @@ const defaultProps = { onUnmount: noop, raf: true, renderOnComponentChange: true, + fiber: PixiFiber }; export function getCanvasProps(props) @@ -117,6 +120,7 @@ class Stage extends React.Component _mediaQuery = null; _ticker = null; _needsUpdate = true; + _fiber = PixiFiber; app = null; componentDidMount() @@ -128,8 +132,12 @@ class Stage extends React.Component options, raf, renderOnComponentChange, + fiber, } = this.props; + // We save the fiber from props because we can't support changing it + this._fiber = fiber; + this.app = new Application({ width, height, @@ -152,8 +160,19 @@ class Stage extends React.Component this.app.ticker[raf ? 'start' : 'stop'](); this.app.stage.__reactpixi = { root: this.app.stage }; - this.mountNode = PixiFiber.createContainer(this.app.stage); - PixiFiber.updateContainer(this.getChildren(), this.mountNode, this); + + this.mountNode = this._fiber.createContainer( + this.app.stage, + 0, + null, + false, + null, + '', + (error) => console.error('React recoverable error:', error), + null + ); + + this._fiber.updateContainer(this.getChildren(), this.mountNode, this); onMount(this.app); @@ -219,7 +238,7 @@ class Stage extends React.Component } // flush fiber - PixiFiber.updateContainer(this.getChildren(), this.mountNode, this); + this._fiber.updateContainer(this.getChildren(), this.mountNode, this); if ( prevProps.width !== width @@ -306,7 +325,7 @@ class Stage extends React.Component this.needsRenderUpdate ); - PixiFiber.updateContainer(null, this.mountNode, this); + this._fiber.updateContainer(null, this.mountNode, this); if (this._mediaQuery) { diff --git a/packages/react/test/__snapshots__/index.spec.js.snap b/packages/react/test/__snapshots__/index.spec.js.snap index fb434a12..eb14d275 100644 --- a/packages/react/test/__snapshots__/index.spec.js.snap +++ b/packages/react/test/__snapshots__/index.spec.js.snap @@ -100,6 +100,7 @@ exports[`index export modules for index 1`] = ` "shouldSuspend": [Function], "updateContainer": [Function], }, + "Reconciler": [Function], "SimpleMesh": "SimpleMesh", "SimpleRope": "SimpleRope", "Sprite": "Sprite", @@ -151,6 +152,62 @@ exports[`index export modules for index 1`] = ` "touchmove", "touchstart", ], + "hostconfig": { + "afterActiveInstanceBlur": [Function], + "appendChild": [Function], + "appendChildToContainer": [Function], + "appendInitialChild": [Function], + "beforeActiveInstanceBlur": [Function], + "cancelTimeout": [Function], + "clearContainer": [Function], + "commitMount": [Function], + "commitTextUpdate": [Function], + "commitUpdate": [Function], + "createInstance": [Function], + "createTextInstance": [Function], + "detachDeletedInstance": [Function], + "finalizeInitialChildren": [Function], + "getChildHostContext": [Function], + "getChildHostContextForEventComponent": [Function], + "getCurrentEventPriority": [Function], + "getFundamentalComponentInstance": [Function], + "getInstanceFromNode": [Function], + "getPublicInstance": [Function], + "getRootHostContext": [Function], + "handleEventTarget": [Function], + "hideInstance": [Function], + "insertBefore": [Function], + "insertInContainerBefore": [Function], + "isOpaqueHydratingObject": [Function], + "isPrimaryRenderer": false, + "makeClientIdInDEV": [Function], + "makeOpaqueHydratingObject": [Function], + "mountEventComponent": [Function], + "mountFundamentalComponent": [Function], + "noTimeout": -1, + "now": [Function], + "prepareForCommit": [Function], + "preparePortalMount": [Function], + "prepareUpdate": [Function], + "removeChild": [Function], + "removeChildFromContainer": [Function], + "resetAfterCommit": [Function], + "resetTextContent": [Function], + "scheduleMicrotask": [Function], + "scheduleTimeout": [Function], + "shouldDeprioritizeSubtree": [Function], + "shouldSetTextContent": [Function], + "shouldUpdateFundamentalComponent": [Function], + "supportsHydration": false, + "supportsMicrotasks": true, + "supportsMutation": true, + "supportsPersistence": false, + "unhideInstance": [Function], + "unhideTextInstance": [Function], + "unmountFundamentalComponent": [Function], + "updateEventComponent": [Function], + "warnsIfNotActing": false, + }, "render": [Function], "unmountComponentAtNode": [Function], "useApp": [Function], diff --git a/packages/react/test/__snapshots__/stage.spec.js.snap b/packages/react/test/__snapshots__/stage.spec.js.snap index 76fed5b2..2c0aca78 100644 --- a/packages/react/test/__snapshots__/stage.spec.js.snap +++ b/packages/react/test/__snapshots__/stage.spec.js.snap @@ -3,6 +3,7 @@ exports[`stage prop types 1`] = ` { "children": [Function], + "fiber": [Function], "height": [Function], "onMount": [Function], "onUnmount": [Function], @@ -15,6 +16,43 @@ exports[`stage prop types 1`] = ` exports[`stage prop types 2`] = ` { + "fiber": { + "attemptContinuousHydration": [MockFunction], + "attemptDiscreteHydration": [MockFunction], + "attemptHydrationAtCurrentPriority": [MockFunction], + "attemptSynchronousHydration": [MockFunction], + "batchedUpdates": [MockFunction], + "createComponentSelector": [MockFunction], + "createContainer": [MockFunction], + "createHasPseudoClassSelector": [MockFunction], + "createHydrationContainer": [MockFunction], + "createPortal": [MockFunction], + "createRoleSelector": [MockFunction], + "createTestNameSelector": [MockFunction], + "createTextSelector": [MockFunction], + "deferredUpdates": [MockFunction], + "discreteUpdates": [MockFunction], + "findAllNodes": [MockFunction], + "findBoundingRects": [MockFunction], + "findHostInstance": [MockFunction], + "findHostInstanceWithNoPortals": [MockFunction], + "findHostInstanceWithWarning": [MockFunction], + "flushControlled": [MockFunction], + "flushPassiveEffects": [MockFunction], + "flushSync": [MockFunction], + "focusWithin": [MockFunction], + "getCurrentUpdatePriority": [MockFunction], + "getFindAllNodesFailureDescription": [MockFunction], + "getPublicRootInstance": [MockFunction], + "injectIntoDevTools": [MockFunction], + "isAlreadyRendering": [MockFunction], + "observeVisibleRects": [MockFunction], + "registerMutableSourceForHydration": [MockFunction], + "runWithPriority": [MockFunction], + "shouldError": [MockFunction], + "shouldSuspend": [MockFunction], + "updateContainer": [MockFunction], + }, "height": 600, "onMount": [Function], "onUnmount": [Function], diff --git a/packages/react/test/render.spec.js b/packages/react/test/render.spec.js index a4049a10..507d12c7 100644 --- a/packages/react/test/render.spec.js +++ b/packages/react/test/render.spec.js @@ -53,9 +53,6 @@ describe('render', () => { renderElementToStage(); expect(PixiFiber.createContainer).toHaveBeenCalledTimes(1); - expect(PixiFiber.createContainer).toHaveBeenLastCalledWith( - app.stage, - ); }); test('call updateContainer', () => diff --git a/packages/react/test/stage.spec.js b/packages/react/test/stage.spec.js index af1e36eb..434d819d 100644 --- a/packages/react/test/stage.spec.js +++ b/packages/react/test/stage.spec.js @@ -1,9 +1,11 @@ import React from 'react'; +import Reconciler from 'react-reconciler'; import { Application } from '@pixi/app'; import { Container as PixiContainer } from '@pixi/display'; import renderer, { act } from 'react-test-renderer'; import * as reactTest from '@testing-library/react'; import { PixiFiber } from '../src'; +import hostconfig from '../src/reconciler/hostconfig'; import { Container, Stage, Text } from '../src'; import { Context } from '../src/stage/provider'; import { getCanvasProps } from '../src/stage'; @@ -194,7 +196,6 @@ describe('stage', () => const stage = el.getInstance().app.stage; expect(PixiFiber.createContainer).toHaveBeenCalledTimes(1); - expect(PixiFiber.createContainer).toHaveBeenCalledWith(stage); }); test('call PixiFiber.updateContainer on componentDidMount', () => @@ -241,6 +242,22 @@ describe('stage', () => expect(PixiFiber.updateContainer).toHaveBeenCalledWith(null, instance.mountNode, instance); }); + test('using a custom fiber', () => + { + const customHostconfig = { + ...hostconfig, + createInstance: jest.fn(hostconfig.createInstance) + }; + + const customFiber = Reconciler(customHostconfig); + + const { unmount } = reactTest.render(); + + unmount(); + + expect(customHostconfig.createInstance).toHaveBeenCalledTimes(1); + }); + describe('pixi application', () => { test('ticker running on mount', () =>