Skip to content

Commit

Permalink
Add createRoot API (#392)
Browse files Browse the repository at this point in the history
* add createRoot api

* Update docs with createRoot API

---------

Co-authored-by: Alex Prokop <[email protected]>
  • Loading branch information
baseten and Alex Prokop authored Jan 31, 2023
1 parent b68c107 commit 82ae3e5
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 166 deletions.
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

0 comments on commit 82ae3e5

Please sign in to comment.