Skip to content

Commit 842abe2

Browse files
marklundinCopilot
andauthored
Introduce useAppEvent hook and deprecate useFrame (#186)
* feat: introduce useAppEvent hook and deprecate useFrame - Added a new `useAppEvent` hook for subscribing to PlayCanvas application events with flexible parameter signatures. - Deprecated the `useFrame` hook, recommending the use of `useAppEvent('update', callback)` instead. - Updated exports to reflect the changes and removed the old `useFrame` implementation. * fix: add warning for deprecated useFrame hook - Introduced a warning message in the `useFrame` function to inform users that it is deprecated and will be removed in a future release. Users are encouraged to use `useAppEvent('update', callback)` instead. * feat: implement useAppEvent hook and deprecate useFrame - Added the `useAppEvent` hook for subscribing to PlayCanvas application events, including 'update', 'prerender', and 'postrender'. - Updated documentation to reflect the deprecation of the `useFrame` hook, providing migration guidance for users. - Introduced tests for the `useAppEvent` hook to ensure proper registration and error handling. - Removed the `useFrame` hook and its associated tests. * refactor: enhance useAppEvent hook with improved type safety - Simplified the event callback types by introducing a mapped type for event signatures. - Updated the handler logic to differentiate between event types more clearly. - Ensured proper cleanup of event listeners in the useEffect hook. - Maintained deprecation warning for the useFrame hook, guiding users to use useAppEvent instead. * fix: update import statement to include useAppEvent hook in the MDC rules - Modified the import statement in the PlayCanvas React module to include the newly introduced `useAppEvent` hook, ensuring proper functionality and alignment with recent changes. * refactor: replace useFrame with useAppEvent for rendering on camera change - Updated the rendering logic in the `useRenderOnCameraChange` hook to utilize the `useAppEvent` hook instead of the deprecated `useFrame` hook, ensuring better alignment with the latest event handling practices in PlayCanvas. * Update .changeset/vast-roses-wait.md Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 9dae0be commit 842abe2

File tree

10 files changed

+212
-74
lines changed

10 files changed

+212
-74
lines changed

.changeset/sour-mangos-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@playcanvas/blocks": patch
3+
---
4+
5+
Updated internal api to use `useAppEvent`

.changeset/vast-roses-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@playcanvas/react": minor
3+
---
4+
5+
Deprecate useFrame and introduce useAppEvent API

packages/blocks/src/splat-viewer/hooks/use-render-on-camera-change.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useApp, useFrame } from "@playcanvas/react/hooks";
1+
import { useApp, useAppEvent } from "@playcanvas/react/hooks";
22
import { Entity as PcEntity } from "playcanvas";
33
import { useEffect, useRef } from "react";
44

@@ -51,7 +51,7 @@ export const useRenderOnCameraChange = (entity: PcEntity | null) => {
5151
* However if the canvas is not visible on the page it will take precedence.
5252
* Don't render if the canvas is not visible regardless of any animations.
5353
*/
54-
useFrame(() => {
54+
useAppEvent('update', () => {
5555
if (!entity || !isVisible.current) return;
5656
const world = entity.getWorldTransform().data;
5757
const proj = entity.camera?.projectionMatrix?.data;

packages/docs/content/docs/api/hooks/index.mdx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const ParentEntity = () => {
5454

5555
## useFrame
5656

57+
> **Deprecated**: The `useFrame` hook is deprecated and will be removed in a future release. Please use `useAppEvent('update', callback)` instead.
58+
5759
The `useFrame` hook ties into the event loop and will be called on every frame whilst the component is mounted. Use this when you need to update a value or perform a calculation on every frame. This is better for performance than using react state updates.
5860

5961
```jsx
@@ -68,4 +70,55 @@ const MyComponent = () => {
6870

6971
return <Entity ref={entityRef} />;
7072
}
73+
```
74+
75+
## useAppEvent
76+
77+
The `useAppEvent` hook provides a generic way to subscribe to PlayCanvas application events with proper TypeScript support. It supports different event types with their specific callback signatures.
78+
79+
### Supported Events
80+
81+
- **`'update'`**: Called every frame with delta time `(dt: number) => void`
82+
- **`'prerender'`**: Called before rendering with no parameters `() => void`
83+
- **`'postrender'`**: Called after rendering with no parameters `() => void`
84+
85+
```jsx
86+
import { useAppEvent } from '@playcanvas/react/hooks'
87+
88+
const MyComponent = () => {
89+
const entityRef = useRef(null);
90+
91+
// Frame update with delta time (replaces useFrame)
92+
useAppEvent('update', (dt) => {
93+
entityRef.current.rotate(0, dt, 0);
94+
});
95+
96+
// Pre-render hook
97+
useAppEvent('prerender', () => {
98+
console.log('About to render frame');
99+
});
100+
101+
// Post-render hook
102+
useAppEvent('postrender', () => {
103+
console.log('Finished rendering frame');
104+
});
105+
106+
return <Entity ref={entityRef} />;
107+
}
108+
```
109+
110+
### Migration from useFrame
111+
112+
To migrate from the deprecated `useFrame` hook:
113+
114+
```jsx
115+
// Old way (deprecated)
116+
useFrame((dt) => {
117+
// your frame logic
118+
});
119+
120+
// New way
121+
useAppEvent('update', (dt) => {
122+
// your frame logic
123+
});
71124
```

packages/lib/.playcanvas-react.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Application, Entity } from '@playcanvas/react'
2020
// /components add behaviours to Entities
2121
import { Light, Camera, Render, Gsplat, Screen, Collision, RigigBody, Anim } from '@playcanvas/react/components'
2222
// Hooks add functionality
23-
import { useModel, useSplat, useFrame, useParent, useApp } from '@playcanvas/react/hooks'
23+
import { useModel, useSplat, useAppEvent, useParent, useApp } from '@playcanvas/react/hooks'
2424
```
2525

2626
## Rules for 3D Content

packages/lib/src/hooks/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ export { useApp, AppContext } from './use-app.tsx';
66
export { useParent, ParentContext } from './use-parent.tsx';
77
export { useMaterial } from './use-material.tsx'
88
export { useAsset, useSplat, useTexture, useEnvAtlas, useModel, useFont } from './use-asset.ts';
9-
export { useFrame } from './use-frame.tsx';
9+
export { useFrame, useAppEvent } from './use-app-event.ts';
1010
export type { AssetResult } from './use-asset.ts';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { renderHook } from '@testing-library/react';
3+
import { useAppEvent } from './use-app-event.ts';
4+
import { Application } from '../Application.tsx';
5+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6+
7+
/**
8+
* Note that we can't test the actual firing of the callbacks in tests,
9+
* because these events are not fire in the Null device type.
10+
*
11+
* It's possible that we can run headless once we have a node-wgpu environment.
12+
*/
13+
14+
describe('useAppEvent', () => {
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
});
19+
20+
afterEach(() => {
21+
vi.clearAllMocks();
22+
});
23+
24+
it('should register and unregister callbacks without throwing', () => {
25+
const updateCallback = vi.fn();
26+
const prerenderCallback = vi.fn();
27+
const postrenderCallback = vi.fn();
28+
29+
const { unmount } = renderHook(() => {
30+
useAppEvent('update', updateCallback);
31+
useAppEvent('prerender', prerenderCallback);
32+
useAppEvent('postrender', postrenderCallback);
33+
}, {
34+
wrapper: ({ children }) => <Application deviceTypes={["null"]}>{children}</Application>
35+
});
36+
37+
// Should not throw during registration
38+
expect(updateCallback).toBeDefined();
39+
expect(prerenderCallback).toBeDefined();
40+
expect(postrenderCallback).toBeDefined();
41+
42+
// Should not throw during cleanup
43+
unmount();
44+
});
45+
46+
it('should throw error if app is not available', () => {
47+
const mockCallback = vi.fn();
48+
expect(() => {
49+
renderHook(() => useAppEvent('update', mockCallback));
50+
}).toThrow('`useApp` must be used within an Application component');
51+
});
52+
53+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useEffect, useCallback } from "react";
2+
import { useApp } from "./use-app.tsx";
3+
import { warnOnce } from "../utils/validation.ts"
4+
5+
/**
6+
* Generic hook for subscribing to PlayCanvas application events.
7+
* Supports events with different parameter signatures using TypeScript conditional types.
8+
*
9+
* @param {'update' | 'prerender' | 'postrender'} event - The event name to subscribe to
10+
* @param callback - The callback function to execute when the event fires
11+
*
12+
* @example
13+
* ```tsx
14+
* // Event with delta time - TypeScript will enforce (dt: number) => void
15+
* useAppEvent('update', (dt) => console.log('Frame time:', dt));
16+
*
17+
* // Events with no parameters - TypeScript will enforce () => void
18+
* useAppEvent('prerender', () => console.log('Pre-render'));
19+
* useAppEvent('postrender', () => console.log('Post-render'));
20+
* ```
21+
*
22+
* @example
23+
* ```tsx
24+
* function MyComponent() {
25+
* // Frame update with delta time
26+
* useAppEvent('update', (dt) => {
27+
* console.log('Frame delta time:', dt);
28+
* });
29+
*
30+
* // App lifecycle events
31+
* useAppEvent('prerender', () => {
32+
* console.log('Pre-rendering');
33+
* });
34+
*
35+
* return null;
36+
* }
37+
* ```
38+
*/
39+
type EventCallbackMap = {
40+
/**
41+
* @param dt - The delta time since the last frame
42+
* @returns void
43+
*/
44+
update: (dt: number) => void;
45+
/**
46+
* @returns void
47+
*/
48+
prerender: () => void;
49+
/**
50+
* @returns void
51+
*/
52+
postrender: () => void;
53+
};
54+
55+
type AppEventName = keyof EventCallbackMap;
56+
57+
export function useAppEvent<T extends AppEventName>(
58+
event: T,
59+
callback: EventCallbackMap[T]
60+
): void {
61+
const app = useApp();
62+
63+
const handler = useCallback((dt?: number) => {
64+
if (event === 'update') {
65+
(callback as (dt: number) => void)(dt!);
66+
} else {
67+
(callback as () => void)();
68+
}
69+
}, [callback, event]);
70+
71+
useEffect(() => {
72+
if (!app) {
73+
throw new Error("`useAppEvent` must be used inside an `<Application />` component");
74+
}
75+
76+
app.on(event, handler);
77+
return () => {
78+
app.off(event, handler);
79+
};
80+
}, [app, handler, event]);
81+
}
82+
83+
/**
84+
* useFrame hook — registers a callback on every frame update.
85+
* The callback receives the delta time (dt) since the last frame.
86+
*
87+
* @deprecated Use useAppEvent('update', callback) instead
88+
*/
89+
export const useFrame = (callback: (dt: number) => void) => {
90+
warnOnce("`useFrame` is deprecated and will be removed in a future release. Please use useAppEvent('update', callback) instead.")
91+
return useAppEvent('update', callback);
92+
};

packages/lib/src/hooks/use-frame.test.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

packages/lib/src/hooks/use-frame.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)