Skip to content

Commit

Permalink
Merge pull request #515 from pixijs/506-bug-usetick-not-ticking-on-in…
Browse files Browse the repository at this point in the history
…itial-load-during-vite-dev

Send app state to context
  • Loading branch information
trezy authored Jul 31, 2024
2 parents 3f10e5a + 0987eba commit f149527
Show file tree
Hide file tree
Showing 23 changed files with 249 additions and 115 deletions.
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

0 comments on commit f149527

Please sign in to comment.