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

Send app state to context #515

Merged
1 change: 0 additions & 1 deletion .codesandbox/ci.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"buildCommand": "codesandbox-ci",
"sandboxes": ["/.codesandbox/sandbox"],
"node": "18"
}
28 changes: 19 additions & 9 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
name: "Install Project"
name: "Setup the project"
description: "Installs node, npm, and dependencies"

runs:
using: "composite"
steps:
- name: Use Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org"

- name: Cache Dependencies
id: node-modules-cache
- name: Get npm cache directory
id: npm-cache-dir
shell: bash
run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}

- name: Cache dependencies
uses: actions/cache@v4
id: npm-cache
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
${{ runner.os }}-node-

- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
shell: bash
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Install Dependencies
- name: Rebuild binaries
if: steps.node-modules-cache.outputs.cache-hit != 'true'
shell: bash
run: npm ci
run: npm rebuild
35 changes: 29 additions & 6 deletions .github/workflows/handle-release-branch-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,45 @@ name: Handle Release Branch Push

on:
push:
branches:
- 'alpha'
- 'beta'
- 'main'

jobs:
release:
verify:
name: Verify
runs-on: ubuntu-latest
strategy:
matrix:
script:
# - name: Typecheck
# command: test:types
- name: Lint
command: test:lint
- name: Unit tests
command: test
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup
uses: ./.github/actions/setup

- name: ${{ matrix.script.name }}
run: npm run ${{ matrix.script.command }}

publish:
name: Publish
needs:
- verify
if: contains(fromJson('["refs/heads/alpha", "refs/heads/beta", "refs/heads/main"]'), github.ref)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Publish Release
- name: Publish release
uses: ./.github/actions/publish-release
with:
branchName: ${{ github.head_ref || github.ref_name }}
Expand Down
24 changes: 0 additions & 24 deletions .github/workflows/main.yml

This file was deleted.

55 changes: 45 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ To add to an existing React application, just install the dependencies:

#### Install Pixi React Dependencies
```bash
npm install pixi.js@^8.2.1 @pixi/react
npm install pixi.js@^8.2.1 @pixi/react@beta
```

#### Pixie React Usage
Expand Down Expand Up @@ -189,7 +189,7 @@ Pixi React supports custom components via the `extend` API. For example, you can
import { extend } from '@pixi/react'
import { Viewport } from 'pixi-viewport'

extend({ viewport })
extend({ Viewport })

const MyComponent = () => {
<viewport>
Expand Down Expand Up @@ -219,36 +219,40 @@ declare global {

#### `useApp`

`useApp` allows access to the parent `PIXI.Application` created by the `<Application>` component. This hook _will not work_ outside of an `<Application>` component. Additionally, the parent application is passed via [React Context](https://react.dev/reference/react/useContext). This means `useApp` will only work appropriately in _child components_, and not directly in the component that contains the `<Application>` component.
**DEPRECATED.** Use `useApplication` hook instead.

For example, the following example `useApp` **will not** be able to access the parent application:
#### `useApplication`

`useApplication` allows access to the parent `PIXI.Application` created by the `<Application>` component. This hook _will not work_ outside of an `<Application>` component. Additionally, the parent application is passed via [React Context](https://react.dev/reference/react/useContext). This means `useApplication` will only work appropriately in _child components_, and in the same component that creates the `<Application>`.

For example, the following example `useApplication` **will not** be able to access the parent application:

```jsx
import {
Application,
useApp,
useApplication,
} from '@pixi/react'

const ParentComponent = () => {
// This will cause an invariant violation.
const app = useApp()
const { app } = useApplication()

return (
<Application />
)
}
```

Here's a working example where `useApp` **will** be able to access the parent application:
Here's a working example where `useApplication` **will** be able to access the parent application:

```jsx
import {
Application,
useApp,
useApplication,
} from '@pixi/react'

const ChildComponent = () => {
const app = useApp()
const { app } = useApplication()

console.log(app)

Expand Down Expand Up @@ -357,7 +361,7 @@ const MyComponent = () => {
}
```

`useTick` optionally takes a boolean as a second argument. Setting this boolean to `false` will cause the callback to be disabled until the argument is set to true again.
`useTick` optionally takes an options object. This allows control of all [`ticker.add`](https://pixijs.download/release/docs/ticker.Ticker.html#add) options, as well as adding the `isEnabled` option. Setting `isEnabled` to `false` will cause the callback to be disabled until the argument is changed to true again.

```jsx
import { useState } from 'react'
Expand All @@ -373,3 +377,34 @@ const MyComponent = () => {
)
}
```

> [!CAUTION]
> The callback passed to `useTick` **is not memoised**. This can cause issues where your callback is being removed and added back to the ticker on every frame if you're mutating state in a component where `useTick` is using a non-memoised function. For example, this issue would affect the component below because we are mutating the state, causing the component to re-render constantly:
> ```jsx
> import { useState } from 'react'
> import { useTick } from '@pixi/react'
>
> const MyComponent = () => {
> const [count, setCount] = useState(0)
>
> useTick(() => setCount(previousCount => previousCount + 1))
>
> return null
> }
> ```
> This issue can be solved by memoising the callback passed to `useTick`:
> ```jsx
> import {
> useCallback,
> useState,
> } from 'react'
> import { useTick } from '@pixi/react'
>
> const MyComponent = () => {
> const [count, setCount] = useState(0)
>
> const updateCount = useCallback(() => setCount(previousCount => previousCount + 1), [])
>
> useTick(updateCount)
> }
> ```
2 changes: 1 addition & 1 deletion src/components/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const ApplicationFunction: ForwardRefRenderFunction<PixiApplication, Appl

useIsomorphicLayoutEffect(() =>
{
const canvasElement = canvasRef.current as HTMLCanvasElement;
const canvasElement = canvasRef.current;

if (canvasElement)
{
Expand Down
4 changes: 2 additions & 2 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createContext } from 'react';

import type { InternalState } from '../typedefs/InternalState.ts';
import type { ApplicationState } from '../typedefs/ApplicationState.ts';

export const Context = createContext<Partial<InternalState>>({});
export const Context = createContext<ApplicationState>({} as ApplicationState);

export const ContextProvider = Context.Provider;
export const ContextConsumer = Context.Consumer;
47 changes: 32 additions & 15 deletions src/core/createRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,47 @@ import { roots } from './roots.ts';

import type { ApplicationOptions } from 'pixi.js';
import type { ReactNode } from 'react';
import type { ApplicationState } from '../typedefs/ApplicationState.ts';
import type { CreateRootOptions } from '../typedefs/CreateRootOptions.ts';
import type { HostConfig } from '../typedefs/HostConfig.ts';
import type { InternalState } from '../typedefs/InternalState.ts';

/** Creates a new root for a Pixi React app. */
export function createRoot(
/** @description The DOM node which will serve as the root for this tree. */
target: HTMLElement | HTMLCanvasElement,
options: Partial<InternalState> = {},

/** @description Options to configure the tree. */
options: CreateRootOptions = {},

/**
* @deprecated
* @description Callback to be fired when the application finishes initializing.
*/
onInit?: (app: Application) => void,
)
{
// Check against mistaken use of createRoot
let root = roots.get(target);
let applicationState = (root?.applicationState ?? {
isInitialised: false,
isInitialising: false,
}) as ApplicationState;

const state = Object.assign((root?.state ?? {}), options) as InternalState;
const internalState = root?.internalState ?? {} as InternalState;

if (root)
{
log('warn', 'createRoot should only be called once!');
}
else
{
state.app = new Application();
state.rootContainer = prepareInstance(state.app.stage) as HostConfig['containerInstance'];
applicationState.app = new Application();
internalState.rootContainer = prepareInstance(applicationState.app.stage) as HostConfig['containerInstance'];
}

const fiber = root?.fiber ?? reconciler.createContainer(
state.rootContainer,
internalState.rootContainer,
ConcurrentRoot,
null,
false,
Expand Down Expand Up @@ -67,15 +81,17 @@ export function createRoot(
applicationOptions: ApplicationOptions,
) =>
{
if (!state.app.renderer && !state.isInitialising)
if (!applicationState.app.renderer && !applicationState.isInitialised && !applicationState.isInitialising)
{
state.isInitialising = true;
await state.app.init({
applicationState.isInitialising = true;
await applicationState.app.init({
...applicationOptions,
canvas,
});
onInit?.(state.app);
state.isInitialising = false;
applicationState.isInitialising = false;
applicationState.isInitialised = true;
applicationState = { ...applicationState };
(options.onInit ?? onInit)?.(applicationState.app);
}

Object.entries(applicationOptions).forEach(([key, value]) =>
Expand All @@ -91,24 +107,25 @@ export function createRoot(
}

// @ts-expect-error Typescript doesn't realise it, but we're already verifying that this isn't a readonly key.
state.app[typedKey] = value;
applicationState.app[typedKey] = value;
});

// Update fiber and expose Pixi.js state to children
reconciler.updateContainer(
createElement(ContextProvider, { value: state }, children),
createElement(ContextProvider, { value: applicationState }, children),
fiber,
null,
() => undefined
() => undefined,
);

return state.app;
return applicationState.app;
};

root = {
applicationState,
fiber,
internalState,
render,
state,
};

roots.set(canvas, root);
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/applyProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
} from 'pixi.js';
import type { DiffSet } from '../typedefs/DiffSet.ts';
import type { HostConfig } from '../typedefs/HostConfig.ts';
import type { NodeState } from '../typedefs/NodeState.ts';
import type { InstanceState } from '../typedefs/InstanceState.ts';

const DEFAULT = '__default';
const DEFAULTS_CONTAINERS = new Map();
Expand Down Expand Up @@ -53,7 +53,7 @@ export function applyProps(
{
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__pixireact: instanceState = {} as NodeState,
__pixireact: instanceState = {} as InstanceState,
...instanceProps
} = instance;

Expand Down
4 changes: 2 additions & 2 deletions src/helpers/prepareInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type {
Filter,
} from 'pixi.js';
import type { HostConfig } from '../typedefs/HostConfig.ts';
import type { NodeState } from '../typedefs/NodeState.ts';
import type { InstanceState } from '../typedefs/InstanceState.ts';

/** Create the instance with the provided sate and attach the component to it. */
export function prepareInstance<T extends Container | Filter | HostConfig['instance']>(
component: T,
state: Partial<NodeState> = {},
state: Partial<InstanceState> = {},
)
{
const instance = component as HostConfig['instance'];
Expand Down
Loading