Skip to content

Commit f149527

Browse files
authored
Merge pull request #515 from pixijs/506-bug-usetick-not-ticking-on-initial-load-during-vite-dev
Send app state to context
2 parents 3f10e5a + 0987eba commit f149527

23 files changed

+249
-115
lines changed

.codesandbox/ci.json

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"buildCommand": "codesandbox-ci",
32
"sandboxes": ["/.codesandbox/sandbox"],
43
"node": "18"
54
}

.github/actions/setup/action.yml

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
1-
name: "Install Project"
1+
name: "Setup the project"
22
description: "Installs node, npm, and dependencies"
33

44
runs:
55
using: "composite"
66
steps:
7-
- name: Use Node.js
7+
- name: Setup Node.js
88
uses: actions/setup-node@v4
99
with:
1010
node-version: lts/*
1111
registry-url: "https://registry.npmjs.org"
1212

13-
- name: Cache Dependencies
14-
id: node-modules-cache
13+
- name: Get npm cache directory
14+
id: npm-cache-dir
15+
shell: bash
16+
run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
17+
18+
- name: Cache dependencies
1519
uses: actions/cache@v4
20+
id: npm-cache
1621
with:
17-
path: node_modules
18-
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
22+
path: ${{ steps.npm-cache-dir.outputs.dir }}
23+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
1924
restore-keys: |
20-
${{ runner.os }}-node-modules-
25+
${{ runner.os }}-node-
26+
27+
- name: Install dependencies
28+
if: steps.node-modules-cache.outputs.cache-hit != 'true'
29+
shell: bash
30+
run: npm ci --ignore-scripts --no-audit --no-fund
2131

22-
- name: Install Dependencies
32+
- name: Rebuild binaries
2333
if: steps.node-modules-cache.outputs.cache-hit != 'true'
2434
shell: bash
25-
run: npm ci
35+
run: npm rebuild

.github/workflows/handle-release-branch-push.yml

+29-6
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,45 @@ name: Handle Release Branch Push
22

33
on:
44
push:
5-
branches:
6-
- 'alpha'
7-
- 'beta'
8-
- 'main'
95

106
jobs:
11-
release:
7+
verify:
8+
name: Verify
129
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
script:
13+
# - name: Typecheck
14+
# command: test:types
15+
- name: Lint
16+
command: test:lint
17+
- name: Unit tests
18+
command: test
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Setup
26+
uses: ./.github/actions/setup
1327

28+
- name: ${{ matrix.script.name }}
29+
run: npm run ${{ matrix.script.command }}
30+
31+
publish:
32+
name: Publish
33+
needs:
34+
- verify
35+
if: contains(fromJson('["refs/heads/alpha", "refs/heads/beta", "refs/heads/main"]'), github.ref)
36+
runs-on: ubuntu-latest
1437
steps:
1538
- name: Checkout
1639
uses: actions/checkout@v4
1740
with:
1841
fetch-depth: 0
1942

20-
- name: Publish Release
43+
- name: Publish release
2144
uses: ./.github/actions/publish-release
2245
with:
2346
branchName: ${{ github.head_ref || github.ref_name }}

.github/workflows/main.yml

-24
This file was deleted.

README.md

+45-10
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ To add to an existing React application, just install the dependencies:
4040

4141
#### Install Pixi React Dependencies
4242
```bash
43-
npm install pixi.js@^8.2.1 @pixi/react
43+
npm install pixi.js@^8.2.1 @pixi/react@beta
4444
```
4545

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

192-
extend({ viewport })
192+
extend({ Viewport })
193193

194194
const MyComponent = () => {
195195
<viewport>
@@ -219,36 +219,40 @@ declare global {
219219

220220
#### `useApp`
221221

222-
`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.
222+
**DEPRECATED.** Use `useApplication` hook instead.
223223

224-
For example, the following example `useApp` **will not** be able to access the parent application:
224+
#### `useApplication`
225+
226+
`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>`.
227+
228+
For example, the following example `useApplication` **will not** be able to access the parent application:
225229

226230
```jsx
227231
import {
228232
Application,
229-
useApp,
233+
useApplication,
230234
} from '@pixi/react'
231235

232236
const ParentComponent = () => {
233237
// This will cause an invariant violation.
234-
const app = useApp()
238+
const { app } = useApplication()
235239

236240
return (
237241
<Application />
238242
)
239243
}
240244
```
241245

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

244248
```jsx
245249
import {
246250
Application,
247-
useApp,
251+
useApplication,
248252
} from '@pixi/react'
249253

250254
const ChildComponent = () => {
251-
const app = useApp()
255+
const { app } = useApplication()
252256

253257
console.log(app)
254258

@@ -357,7 +361,7 @@ const MyComponent = () => {
357361
}
358362
```
359363

360-
`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.
364+
`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.
361365

362366
```jsx
363367
import { useState } from 'react'
@@ -373,3 +377,34 @@ const MyComponent = () => {
373377
)
374378
}
375379
```
380+
381+
> [!CAUTION]
382+
> 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:
383+
> ```jsx
384+
> import { useState } from 'react'
385+
> import { useTick } from '@pixi/react'
386+
>
387+
> const MyComponent = () => {
388+
> const [count, setCount] = useState(0)
389+
>
390+
> useTick(() => setCount(previousCount => previousCount + 1))
391+
>
392+
> return null
393+
> }
394+
> ```
395+
> This issue can be solved by memoising the callback passed to `useTick`:
396+
> ```jsx
397+
> import {
398+
> useCallback,
399+
> useState,
400+
> } from 'react'
401+
> import { useTick } from '@pixi/react'
402+
>
403+
> const MyComponent = () => {
404+
> const [count, setCount] = useState(0)
405+
>
406+
> const updateCount = useCallback(() => setCount(previousCount => previousCount + 1), [])
407+
>
408+
> useTick(updateCount)
409+
> }
410+
> ```

src/components/Application.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const ApplicationFunction: ForwardRefRenderFunction<PixiApplication, Appl
9797

9898
useIsomorphicLayoutEffect(() =>
9999
{
100-
const canvasElement = canvasRef.current as HTMLCanvasElement;
100+
const canvasElement = canvasRef.current;
101101

102102
if (canvasElement)
103103
{

src/components/Context.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { createContext } from 'react';
22

3-
import type { InternalState } from '../typedefs/InternalState.ts';
3+
import type { ApplicationState } from '../typedefs/ApplicationState.ts';
44

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

77
export const ContextProvider = Context.Provider;
88
export const ContextConsumer = Context.Consumer;

src/core/createRoot.ts

+32-15
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,47 @@ import { roots } from './roots.ts';
1010

1111
import type { ApplicationOptions } from 'pixi.js';
1212
import type { ReactNode } from 'react';
13+
import type { ApplicationState } from '../typedefs/ApplicationState.ts';
14+
import type { CreateRootOptions } from '../typedefs/CreateRootOptions.ts';
1315
import type { HostConfig } from '../typedefs/HostConfig.ts';
1416
import type { InternalState } from '../typedefs/InternalState.ts';
1517

1618
/** Creates a new root for a Pixi React app. */
1719
export function createRoot(
20+
/** @description The DOM node which will serve as the root for this tree. */
1821
target: HTMLElement | HTMLCanvasElement,
19-
options: Partial<InternalState> = {},
22+
23+
/** @description Options to configure the tree. */
24+
options: CreateRootOptions = {},
25+
26+
/**
27+
* @deprecated
28+
* @description Callback to be fired when the application finishes initializing.
29+
*/
2030
onInit?: (app: Application) => void,
2131
)
2232
{
2333
// Check against mistaken use of createRoot
2434
let root = roots.get(target);
35+
let applicationState = (root?.applicationState ?? {
36+
isInitialised: false,
37+
isInitialising: false,
38+
}) as ApplicationState;
2539

26-
const state = Object.assign((root?.state ?? {}), options) as InternalState;
40+
const internalState = root?.internalState ?? {} as InternalState;
2741

2842
if (root)
2943
{
3044
log('warn', 'createRoot should only be called once!');
3145
}
3246
else
3347
{
34-
state.app = new Application();
35-
state.rootContainer = prepareInstance(state.app.stage) as HostConfig['containerInstance'];
48+
applicationState.app = new Application();
49+
internalState.rootContainer = prepareInstance(applicationState.app.stage) as HostConfig['containerInstance'];
3650
}
3751

3852
const fiber = root?.fiber ?? reconciler.createContainer(
39-
state.rootContainer,
53+
internalState.rootContainer,
4054
ConcurrentRoot,
4155
null,
4256
false,
@@ -67,15 +81,17 @@ export function createRoot(
6781
applicationOptions: ApplicationOptions,
6882
) =>
6983
{
70-
if (!state.app.renderer && !state.isInitialising)
84+
if (!applicationState.app.renderer && !applicationState.isInitialised && !applicationState.isInitialising)
7185
{
72-
state.isInitialising = true;
73-
await state.app.init({
86+
applicationState.isInitialising = true;
87+
await applicationState.app.init({
7488
...applicationOptions,
7589
canvas,
7690
});
77-
onInit?.(state.app);
78-
state.isInitialising = false;
91+
applicationState.isInitialising = false;
92+
applicationState.isInitialised = true;
93+
applicationState = { ...applicationState };
94+
(options.onInit ?? onInit)?.(applicationState.app);
7995
}
8096

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

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

97113
// Update fiber and expose Pixi.js state to children
98114
reconciler.updateContainer(
99-
createElement(ContextProvider, { value: state }, children),
115+
createElement(ContextProvider, { value: applicationState }, children),
100116
fiber,
101117
null,
102-
() => undefined
118+
() => undefined,
103119
);
104120

105-
return state.app;
121+
return applicationState.app;
106122
};
107123

108124
root = {
125+
applicationState,
109126
fiber,
127+
internalState,
110128
render,
111-
state,
112129
};
113130

114131
roots.set(canvas, root);

src/helpers/applyProps.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
} from 'pixi.js';
2222
import type { DiffSet } from '../typedefs/DiffSet.ts';
2323
import type { HostConfig } from '../typedefs/HostConfig.ts';
24-
import type { NodeState } from '../typedefs/NodeState.ts';
24+
import type { InstanceState } from '../typedefs/InstanceState.ts';
2525

2626
const DEFAULT = '__default';
2727
const DEFAULTS_CONTAINERS = new Map();
@@ -53,7 +53,7 @@ export function applyProps(
5353
{
5454
const {
5555
// eslint-disable-next-line @typescript-eslint/no-unused-vars
56-
__pixireact: instanceState = {} as NodeState,
56+
__pixireact: instanceState = {} as InstanceState,
5757
...instanceProps
5858
} = instance;
5959

src/helpers/prepareInstance.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import type {
33
Filter,
44
} from 'pixi.js';
55
import type { HostConfig } from '../typedefs/HostConfig.ts';
6-
import type { NodeState } from '../typedefs/NodeState.ts';
6+
import type { InstanceState } from '../typedefs/InstanceState.ts';
77

88
/** Create the instance with the provided sate and attach the component to it. */
99
export function prepareInstance<T extends Container | Filter | HostConfig['instance']>(
1010
component: T,
11-
state: Partial<NodeState> = {},
11+
state: Partial<InstanceState> = {},
1212
)
1313
{
1414
const instance = component as HostConfig['instance'];

0 commit comments

Comments
 (0)