Skip to content

Commit

Permalink
Merge pull request #558 from pixijs/557-bug-context-isnt-available-ac…
Browse files Browse the repository at this point in the history
…ross-the-application-boundary

bug: context isn't available across the application boundary
  • Loading branch information
trezy authored Dec 31, 2024
2 parents 18d52d1 + a827ee6 commit 2f18915
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 54 deletions.
45 changes: 13 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
]
},
"dependencies": {
"its-fine": "^1.2.5",
"react-reconciler": "0.31.0"
},
"devDependencies": {
Expand Down
32 changes: 25 additions & 7 deletions src/components/Application.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
FiberProvider,
useContextBridge,
} from 'its-fine'
import {
type Application as PixiApplication,
extensions as PixiExtensions,
TextStyle,
} from 'pixi.js';
import {
createElement,
forwardRef,
type RefObject,
useCallback,
Expand All @@ -23,7 +26,7 @@ import { type ApplicationRef } from '../typedefs/ApplicationRef';

const originalDefaultTextStyle = { ...TextStyle.defaultTextStyle };

export const Application = forwardRef<ApplicationRef, ApplicationProps>(function Application(
const ApplicationImplementation = forwardRef<ApplicationRef, ApplicationProps>(function Application(
props,
forwardedRef,
)
Expand All @@ -38,6 +41,8 @@ export const Application = forwardRef<ApplicationRef, ApplicationProps>(function
...applicationProps
} = props;

const Bridge = useContextBridge();

const applicationRef: RefObject<PixiApplication | null> = useRef(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const extensionsRef = useRef<Set<any>>(new Set());
Expand Down Expand Up @@ -135,7 +140,8 @@ export const Application = forwardRef<ApplicationRef, ApplicationProps>(function
root = createRoot(canvasElement, {}, handleInit);
}

root.render(children, applicationProps);
// @ts-expect-error The value of `children` is fine, but `PixiReactChildNode` doesn't strictly adhere to the `ReactNode` structure.
root.render((<Bridge>{children}</Bridge>), applicationProps);
}
}, [
applicationProps,
Expand Down Expand Up @@ -177,8 +183,20 @@ export const Application = forwardRef<ApplicationRef, ApplicationProps>(function
}
}, []);

return createElement('canvas', {
className,
ref: canvasRef,
});
return (
<canvas
ref={canvasRef}
className={className} />
);
});

export const Application = forwardRef<ApplicationRef, ApplicationProps>(function ApplicationWrapper(props, ref)
{
return (
<FiberProvider>
<ApplicationImplementation
ref={ref}
{...props} />
</FiberProvider>
);
});
7 changes: 5 additions & 2 deletions src/core/createRoot.ts → src/core/createRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Application } from 'pixi.js';
import { type ApplicationOptions } from 'pixi.js';
import { createElement } from 'react';
import { type ReactNode } from 'react';
import { ConcurrentRoot } from 'react-reconciler/constants.js';
import { ContextProvider } from '../components/Context';
Expand Down Expand Up @@ -115,7 +114,11 @@ export function createRoot(

// Update fiber and expose Pixi.js state to children
reconciler.updateContainer(
createElement(ContextProvider, { value: applicationState }, children),
(
<ContextProvider value={applicationState}>
{children}
</ContextProvider>
),
fiber,
null,
() => undefined,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export { useAssets } from './hooks/useAssets';
export { useExtend } from './hooks/useExtend';
export { useSuspenseAssets } from './hooks/useSuspenseAssets';
export { useTick } from './hooks/useTick';
export { type ApplicationRef } from './typedefs/ApplicationRef';
export { type PixiReactElementProps } from './typedefs/PixiReactNode';
73 changes: 67 additions & 6 deletions test/e2e/components/Application.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { type Application as PixiApplication } from 'pixi.js';
import { useEffect } from 'react';
import { Application as PixiApplication } from 'pixi.js';
import {
createContext,
createRef,
useContext,
useEffect,
} from 'react';
import {
describe,
expect,
Expand All @@ -9,11 +14,65 @@ import {
import { Application } from '../../../src/components/Application';
import { roots } from '../../../src/core/roots';
import { useApplication } from '../../../src/hooks/useApplication';
import { type ApplicationRef } from '../../../src/typedefs/ApplicationRef';
import { isAppMounted } from '../../utils/isAppMounted';
import { render } from '@testing-library/react';
import {
act,
render,
} from '@testing-library/react';

describe('Application', () =>
{
it('mounts correctly', async () =>
{
const renderer = await act(async () => render(<Application />));

expect(renderer.container).toMatchSnapshot();
});

it('forwards its ref', async () =>
{
const onInitSpy = vi.fn();
const ref = createRef<ApplicationRef>();

await act(async () => render((
<Application
ref={ref}
onInit={onInitSpy} />
)));

await expect.poll(() => onInitSpy.mock.calls.length).toEqual(1);

expect(ref.current?.getApplication()).toBeInstanceOf(PixiApplication);
expect(ref.current?.getCanvas()).toBeInstanceOf(HTMLCanvasElement);
});

it('forwards context', async () =>
{
const onInitSpy = vi.fn();
const ParentContext = createContext<boolean>(null!);
let receivedValue!: boolean;

function Test()
{
receivedValue = useContext(ParentContext);

return null;
}

await act(async () => render((
<ParentContext.Provider value={true}>
<Application onInit={onInitSpy}>
<Test />
</Application>
</ParentContext.Provider>
)));

await expect.poll(() => onInitSpy.mock.calls.length).toEqual(1);

expect(receivedValue).toBe(true);
});

describe('onInit', () =>
{
it('runs the callback once', async () =>
Expand All @@ -24,7 +83,9 @@ describe('Application', () =>
<Application onInit={onInitSpy} />
);

render(<TestComponent />);
await act(async () => render((
<TestComponent />
)));

await expect.poll(() => onInitSpy.mock.calls.length).toEqual(1);
});
Expand Down Expand Up @@ -70,7 +131,7 @@ describe('Application', () =>

expect(roots.size).toEqual(0);

const { unmount } = render(<TestComponent />);
const { unmount } = await act(() => render(<TestComponent />));

expect(roots.size).toEqual(1);

Expand Down Expand Up @@ -121,7 +182,7 @@ describe('Application', () =>

expect(roots.size).toEqual(0);

const { unmount } = render(<TestComponent />);
const { unmount } = await act(() => render(<TestComponent />));

expect(roots.size).toEqual(1);

Expand Down
7 changes: 7 additions & 0 deletions test/e2e/components/__snapshots__/Application.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Application > mounts correctly 1`] = `
<div>
<canvas />
</div>
`;
9 changes: 6 additions & 3 deletions test/e2e/hooks/useApplication.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { Application } from '../../../src/components/Application';
import { useApplication } from '../../../src/hooks/useApplication';
import {
act,
render,
renderHook,
} from '@testing-library/react';
Expand Down Expand Up @@ -47,9 +48,9 @@ describe('useApplication', () =>
return null;
};

render(<TestComponent />, {
await act(async () => render(<TestComponent />, {
wrapper: TestComponentWrapper,
});
}));

await new Promise<void>((resolve) =>
{
Expand All @@ -68,6 +69,8 @@ describe('useApplication', () =>

it('throws when not in a React Pixi tree', () =>
{
expect(() => renderHook(() => useApplication())).toThrowError(/no context found/i);
const renderer = () => act(() => renderHook(() => useApplication()));

expect(renderer).toThrowError(/no context found/i);
});
});
9 changes: 5 additions & 4 deletions test/e2e/hooks/useTick.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { Application } from '../../../src/components/Application';
import { useTick } from '../../../src/hooks/useTick';
import {
act,
render,
renderHook,
} from '@testing-library/react';
Expand All @@ -27,7 +28,7 @@ describe('useTick', () =>
return null;
};

render(<TestComponent />, { wrapper: Application });
act(() => render(<TestComponent />, { wrapper: Application }));

await expect.poll(() => useTickSpy.mock.lastCall?.[0]).toBeInstanceOf(Ticker);
});
Expand All @@ -46,16 +47,16 @@ describe('useTick', () =>
return null;
};

render(<TestComponent />, { wrapper: Application });
act(() => render(<TestComponent />, { wrapper: Application }));

await expect.poll(() => useTickSpy.mock.lastCall?.[0]).toBeInstanceOf(Ticker);
});
});

it('throws when not in a React Pixi tree', () =>
{
const result = () => renderHook(() => useTick(() => { /* noop */ }));
const renderer = () => act(() => renderHook(() => useTick(() => { /* noop */ })));

expect(result).toThrowError(/no context found/i);
expect(renderer).toThrowError(/no context found/i);
});
});

0 comments on commit 2f18915

Please sign in to comment.