Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createRoot API #392

Merged
merged 2 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions packages/docs/docs/render/Render.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This is the most common scenario

```jsx
import { Stage, Sprite } from '@pixi/react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';

const App = () => (
<div>
Expand All @@ -19,15 +19,16 @@ const App = () => (
</div>
);

ReactDOM.render(<App />, document.getElementById('root'));
const root = createRoot(document.getElementById('root'));
root.render(<App />);
```

## Custom render call

You can also render a Pixi React component tree directly using the `render` call and bypass ReactDOM entirely:

```jsx
import { render, Text } from '@pixi/react';
import { createRoot, Text } from '@pixi/react';
import { Application } from 'pixi.js';

// Setup PIXI app
Expand All @@ -39,21 +40,22 @@ const app = new Application({
});

// Use the custom renderer to render a valid PIXI object into a PIXI container.
render(<Text text="Hello World" x={200} y={200} />, app.stage);
const root = createRoot(app.stage);
root.render(<Text text="Hello World" x={200} y={200} />);
```

Internally `pixi-react` keeps track of a `roots` list with containers, if you're removing/unmounting the container (or PIXI application),
it's advisable to tear it down correctly. Simply call `unmountComponentAtNode`:
If you're removing/unmounting the container (or PIXI application), it's advisable to tear it down correctly.
Simply call `root.unmount()`:

```jsx
import { render, unmountComponentAtNode, Text } from '@pixi/react';
import { Application } from 'pixi.js';

const app = new Application({...});

render(<Text text="Hello World" />, app.stage);
const root = createRoot(app.stage);
root.render(<Text text="Hello World" />);

// clean up on unmount
// this removes the container from roots list
unmountComponentAtNode(app.stage);
root.unmount();
```
3 changes: 2 additions & 1 deletion packages/react/src/exports.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PixiComponent, TYPES } from './utils/element';
import { render, unmountComponentAtNode } from './render';
import { createRoot, render, unmountComponentAtNode } from './render';
import Stage from './stage';
import { PixiFiber } from './reconciler';
import { Context as AppContext, AppProvider, AppConsumer, withPixiApp } from './stage/provider';
Expand All @@ -15,6 +15,7 @@ import { applyDefaultProps } from './utils/props';
*/

export {
createRoot,
render,
unmountComponentAtNode,
Stage,
Expand Down
37 changes: 37 additions & 0 deletions packages/react/src/reconciler/hostconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,43 @@
*/

import performanceNow from 'performance-now';
import { ContinuousEventPriority, DiscreteEventPriority, DefaultEventPriority } from 'react-reconciler/constants';
import invariant from '../utils/invariant';
import { createElement } from '../utils/element';
import { CHILDREN, applyDefaultProps } from '../utils/props';

const NO_CONTEXT = {};

function getEventPriority()
{
if (typeof window === 'undefined')
{
return DefaultEventPriority;
}

const name = window?.event?.type;

switch (name)
{
case 'click':
case 'contextmenu':
case 'dblclick':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
return DiscreteEventPriority;
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerenter':
case 'pointerleave':
case 'wheel':
return ContinuousEventPriority;
default:
return DefaultEventPriority;
}
}

function appendChild(parent, child)
{
if (parent.addChild)
Expand Down Expand Up @@ -158,6 +189,12 @@ const HostConfig = {
return instance;
},

// TODO: Implement a proper version of getCurrentEventPriority
getCurrentEventPriority()
{
return getEventPriority();
},

prepareForCommit()
{
// noop
Expand Down
111 changes: 92 additions & 19 deletions packages/react/src/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@ import { Container } from '@pixi/display';
import invariant from '../utils/invariant';
import { PixiFiber } from '../reconciler';

// cache root containers
// cache both root PixiFiber containers and React roots
export const roots = new Map();

/**
* Custom Renderer
* @param {Container} container
*/
function unmountComponent(container)
{
invariant(
Container.prototype.isPrototypeOf(container),
'Invalid argument `container`, expected instance of `PIXI.Container`.'
);

if (roots.has(container))
{
const { pixiFiberContainer } = roots.get(container);

// unmount component
PixiFiber.updateContainer(null, pixiFiberContainer, undefined, () =>
{
roots.delete(container);
});
}
}

/**
* Custom Renderer with react 18 API
* Use this without React-DOM
*
* @param {*} element
* @param {PIXI.Container} container (i.e. the Stage)
* @param {Function} callback
* @param {Container} container
* @returns {{ render: Function, unmount: Function}}
*/
export function render(element, container, callback = () => {})
export function createRoot(container)
{
invariant(
Container.prototype.isPrototypeOf(container),
Expand All @@ -22,28 +43,80 @@ export function render(element, container, callback = () => {})

let root = roots.get(container);

invariant(!root, 'Pixi React: createRoot should only be called once');

if (!root)
{
// get the flushed fiber container
root = PixiFiber.createContainer(container);
const pixiFiberContainer = PixiFiber.createContainer(container);

const reactRoot = {
render(element)
{
// schedules a top level update
PixiFiber.updateContainer(
element,
pixiFiberContainer,
undefined
);

return PixiFiber.getPublicRootInstance(pixiFiberContainer);
},
unmount()
{
unmountComponent(container);
roots.delete(container);
},
};

root = { pixiFiberContainer, reactRoot };
roots.set(container, root);
}

// schedules a top level update
PixiFiber.updateContainer(element, root, undefined, callback);

// return the root instance
return PixiFiber.getPublicRootInstance(root);
return root.reactRoot;
}

export function unmountComponentAtNode(container)
/**
* Custom Renderer
* Use this without React-DOM
*
* @deprecated use createRoot instead
*
* @param {React.ReactNode} element
* @param {Container} container (i.e. the Stage)
* @param {Function} callback
*/
export function render(element, container, callback)
{
console.warn(
'Pixi React Deprecation Warning: render is deprecated, use createRoot instead'
);

if (callback !== undefined)
{
console.warn(
'Pixi React Deprecation Warning: render callback no longer exists in React 18'
);
}

let reactRoot;

if (roots.has(container))
{
// unmount component
PixiFiber.updateContainer(null, roots.get(container), undefined, () =>
{
roots.delete(container);
});
({ reactRoot } = roots.get(container));
}
else
{
reactRoot = createRoot(container);
}

return reactRoot.render(element);
}

/**
* @deprecated use root.unmount() instead
* @param {Container} container
*/
export function unmountComponentAtNode(container)
{
unmountComponent(container);
}
1 change: 1 addition & 0 deletions packages/react/test/__snapshots__/index.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ exports[`index export modules for index 1`] = `
"Text": "Text",
"TilingSprite": "TilingSprite",
"applyDefaultProps": [Function],
"createRoot": [Function],
"eventHandlers": [
"click",
"mousedown",
Expand Down
Loading