diff --git a/README.md b/README.md index 6ce80dd2..9b15c3dc 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,10 @@ When an `` is unmounted, it will call the `destroy()` method on the `extensions` is an array of extensions to be loaded. Adding and removing items from this array will automatically load/unload the extensions. The first time this is handled happens before the application is initialised. See Pixi.js's [`extensions`](https://pixijs.download/release/docs/extensions.html) documentation for more info on extensions. +###### `onDestroy` + +If provided, the `onDestroy` callback will be called after the `` component is unmounted and the associated Pixi.js application is destroyed, allowing you to do any additional clean-up or lifecycle handling you may need. + ###### `rendererDestroyOptions` When an `` is unmounted, it will call the `destroy()` method on the Pixi.js application instance. Provide this prop to override the default `RendererDestroyOptions` (the first argument to `destroy()`). See Pixi.js's [`destroy documentation`](https://pixijs.download/release/docs/app.Application.html#destroy) for more info. diff --git a/src/components/Application.tsx b/src/components/Application.tsx index cf3141b6..a7891a3f 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -38,6 +38,7 @@ const ApplicationImplementation = forwardRef(f defaultTextStyle, destroyOptions, extensions, + onDestroy, onInit, rendererDestroyOptions, resizeTo, @@ -98,6 +99,11 @@ const ApplicationImplementation = forwardRef(f onInit?.(application); }, [onInit]); + const handleDestroy = useCallback(() => + { + onDestroy?.(); + }, [onDestroy]); + useIsomorphicLayoutEffect(() => { if (extensions) @@ -142,6 +148,7 @@ const ApplicationImplementation = forwardRef(f { root = createRoot(canvasElement, { destroyOptions, + onDestroy: handleDestroy, onInit: handleInit, rendererDestroyOptions, }); @@ -155,6 +162,7 @@ const ApplicationImplementation = forwardRef(f children, destroyOptions, handleInit, + handleDestroy, rendererDestroyOptions, resizeTo, ]); diff --git a/src/core/createRoot.tsx b/src/core/createRoot.tsx index 875782a3..8eaf36b3 100644 --- a/src/core/createRoot.tsx +++ b/src/core/createRoot.tsx @@ -28,6 +28,7 @@ export function createRoot( destroyOptions: options.destroyOptions, isInitialised: false, isInitialising: false, + onDestroy: options.onDestroy, rendererDestroyOptions: options.rendererDestroyOptions, }) as ApplicationState; diff --git a/src/helpers/unmountRoot.ts b/src/helpers/unmountRoot.ts index d235a987..456c9803 100644 --- a/src/helpers/unmountRoot.ts +++ b/src/helpers/unmountRoot.ts @@ -19,6 +19,7 @@ export function unmountRoot(root: Root) root.applicationState.rendererDestroyOptions, root.applicationState.destroyOptions ); + root.applicationState.onDestroy?.(); } roots.delete(root.internalState.canvas!); diff --git a/src/typedefs/ApplicationProps.ts b/src/typedefs/ApplicationProps.ts index 518ceaeb..84447b94 100644 --- a/src/typedefs/ApplicationProps.ts +++ b/src/typedefs/ApplicationProps.ts @@ -33,6 +33,9 @@ export interface BaseApplicationProps /** @description A unique key which allows React to manage this component across changes in parent state. */ key?: Key, + /** @description Callback to be fired when the application is destroyed. */ + onDestroy?: () => void + /** @description Callback to be fired when the application finishes initializing. */ onInit?: (app: Application) => void diff --git a/src/typedefs/ApplicationState.ts b/src/typedefs/ApplicationState.ts index 611991c7..550174c3 100644 --- a/src/typedefs/ApplicationState.ts +++ b/src/typedefs/ApplicationState.ts @@ -6,5 +6,6 @@ export interface ApplicationState destroyOptions: DestroyOptions; isInitialised: boolean; isInitialising: boolean; + onDestroy?: () => void; rendererDestroyOptions: RendererDestroyOptions; } diff --git a/src/typedefs/CreateRootOptions.ts b/src/typedefs/CreateRootOptions.ts index 2039fa87..91d891b5 100644 --- a/src/typedefs/CreateRootOptions.ts +++ b/src/typedefs/CreateRootOptions.ts @@ -5,6 +5,9 @@ export interface CreateRootOptions /** @description Options to be passed to the application's `destroy` method. */ destroyOptions?: DestroyOptions + /** @description Callback to be fired when the application is destroyed. */ + onDestroy?: () => void + /** @description Callback to be fired when the application finishes initializing. */ onInit?: (app: Application) => void diff --git a/test/e2e/components/Application.test.tsx b/test/e2e/components/Application.test.tsx index 38653707..c2ee6762 100644 --- a/test/e2e/components/Application.test.tsx +++ b/test/e2e/components/Application.test.tsx @@ -260,6 +260,65 @@ describe('Application', () => expect(destroySpy).toHaveBeenCalledWith(rendererDestroyOptions, undefined); }); + it('unmounts with onDestroy callback', async () => + { + let testApp = null as any as PixiApplication; + let testAppIsInitialised = false; + + const onDestroySpy = vi.fn(); + + const TestChildComponent = () => + { + const { + app, + isInitialised, + } = useApplication(); + + useEffect(() => + { + testApp = app; + testAppIsInitialised = isInitialised; + + return () => + { + testApp = app; + testAppIsInitialised = isInitialised; + }; + }, [ + app, + isInitialised, + ]); + + return null; + }; + + const TestComponent = () => ( + + + + ); + + expect(roots.size).toEqual(0); + + const { unmount } = await act(() => render()); + + expect(roots.size).toEqual(1); + + await expect.poll(() => testAppIsInitialised).toEqual(true); + + // sanity check that onDestroy has not yet been called + expect(onDestroySpy).not.toHaveBeenCalled(); + + unmount(); + + expect(roots.size).toEqual(0); + + await expect.poll(() => isAppMounted(testApp)).toBeFalsy(); + + expect(onDestroySpy).toHaveBeenCalledTimes(1); + expect(onDestroySpy).toHaveBeenCalledWith(); + }); + it('unmounts during init', async () => { let testApp = null as any as PixiApplication; diff --git a/test/unit/core/createRoot.test.ts b/test/unit/core/createRoot.test.ts index 871dbe55..de583ecb 100644 --- a/test/unit/core/createRoot.test.ts +++ b/test/unit/core/createRoot.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, + vi, } from 'vitest'; import { createRoot } from '../../../src/core/createRoot'; @@ -61,4 +62,16 @@ describe('createRoot', () => expect(root.applicationState.rendererDestroyOptions).toEqual({ removeView: true }); }); }); + + it('creates a new root with onDestroy callback', () => + { + const onDestroySpy = vi.fn(); + + const target = document.createElement('canvas'); + const root = createRoot(target, { + onDestroy: onDestroySpy, + }); + + expect(root.applicationState.onDestroy).toBe(onDestroySpy); + }); });