Skip to content

Commit 9c0f48a

Browse files
committed
Enable multiple useCanvasEffect on the same Canvas. Run callbacks as soon as the size is available.
1 parent 37ff2e3 commit 9c0f48a

2 files changed

Lines changed: 75 additions & 22 deletions

File tree

packages/webgpu/src/Canvas.tsx

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { ViewProps, LayoutChangeEvent } from "react-native";
22
import { View } from "react-native";
33
import {
44
forwardRef,
5-
useEffect,
65
useImperativeHandle,
76
useRef,
87
useState,
@@ -12,6 +11,7 @@ import {
1211
import type { RefObject } from "react";
1312

1413
import WebGPUNativeView from "./WebGPUViewNativeComponent";
14+
import { useSignal } from "./useSignal";
1515

1616
let CONTEXT_COUNTER = 1;
1717
function generateContextId() {
@@ -60,34 +60,38 @@ interface Size {
6060
height: number;
6161
}
6262

63-
const useSizeFabric = (ref: RefObject<View>) => {
64-
const [size, setSize] = useState<null | Size>(null);
63+
const useSizeFabric = (ref: RefObject<View>, onSizeChange: (v: Size) => void) => {
64+
const [sizeSignal] = useSignal<null | Size>(null);
6565
useLayoutEffect(() => {
6666
if (!ref.current) {
6767
throw new Error("Canvas ref is null");
6868
}
6969
ref.current.measureInWindow((_x, _y, width, height) => {
70-
setSize({ width, height });
70+
const size: Size = { width, height };
71+
sizeSignal.set(size);
72+
onSizeChange(size);
7173
});
7274
}, [ref]);
73-
return { size, onLayout: undefined };
75+
return { sizeSignal, onLayout: undefined };
7476
};
7577

76-
const useSizePaper = (_ref: RefObject<View>) => {
77-
const [size, setSize] = useState<null | Size>(null);
78+
const useSizePaper = (_ref: RefObject<View>, onSizeChange: (v: Size) => void) => {
79+
const [sizeSignal] = useSignal<null | Size>(null);
7880
const onLayout = useCallback<(event: LayoutChangeEvent) => void>(
7981
({
8082
nativeEvent: {
8183
layout: { width, height },
8284
},
8385
}) => {
84-
if (size === null) {
85-
setSize({ width, height });
86+
if (sizeSignal.get() === null) {
87+
const size: Size = { width, height };
88+
sizeSignal.set(size);
89+
onSizeChange(size);
8690
}
8791
},
88-
[size],
92+
[sizeSignal],
8993
);
90-
return { size, onLayout };
94+
return { sizeSignal, onLayout };
9195
};
9296

9397
export const Canvas = forwardRef<
@@ -97,30 +101,33 @@ export const Canvas = forwardRef<
97101
const viewRef = useRef(null);
98102
const FABRIC = RNWebGPU.fabric;
99103
const useSize = FABRIC ? useSizeFabric : useSizePaper;
100-
const [contextId, _] = useState(() => generateContextId());
101-
const cb = useRef<() => void>();
102-
const { size, onLayout } = useSize(viewRef);
103-
useEffect(() => {
104-
if (size && cb.current) {
105-
cb.current();
106-
}
107-
}, [size]);
104+
const [contextId, _] = useState(generateContextId);
105+
const whenReadyCallbacks = useRef<(() => void)[]>([]);
106+
const onSizeChange = useCallback(() => {
107+
// The size of the canvas has been computed, meaning we're ready
108+
// to display things on it!
109+
whenReadyCallbacks.current.forEach(cb => cb());
110+
whenReadyCallbacks.current = [];
111+
}, []);
112+
const { sizeSignal, onLayout } = useSize(viewRef, onSizeChange);
113+
108114
useImperativeHandle(ref, () => ({
109115
getContextId: () => contextId,
110116
getNativeSurface: () => {
111-
if (size === null) {
117+
if (sizeSignal.get() === null) {
112118
throw new Error("[WebGPU] Canvas size is not available yet");
113119
}
114120
return RNWebGPU.getNativeSurface(contextId);
115121
},
116122
whenReady(callback: () => void) {
117-
if (size === null) {
118-
cb.current = callback;
123+
if (sizeSignal.get() === null) {
124+
whenReadyCallbacks.current.push(callback);
119125
} else {
120126
callback();
121127
}
122128
},
123129
getContext(contextName: "webgpu"): RNCanvasContext | null {
130+
const size = sizeSignal.get();
124131
if (contextName !== "webgpu") {
125132
throw new Error(`[WebGPU] Unsupported context: ${contextName}`);
126133
}

packages/webgpu/src/useSignal.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useReducer, useState } from "react";
2+
3+
export interface Signal<T> {
4+
/** Get latest value */
5+
get(): T;
6+
set(value: T): void;
7+
}
8+
9+
function emptyReducer<T>(value: T): T {
10+
return value;
11+
}
12+
13+
interface CreateSignalOptions<T> {
14+
initialValue: T;
15+
setValue(v: T): void;
16+
}
17+
18+
function createSignal<T>(options: CreateSignalOptions<T>): Signal<T> {
19+
let value = options.initialValue;
20+
21+
return {
22+
get(): T {
23+
return value;
24+
},
25+
set(newValue: T): void {
26+
value = newValue;
27+
options.setValue(newValue);
28+
},
29+
};
30+
}
31+
32+
/**
33+
* A replacement for `useState` that returns a "Signal", which in this case is an object
34+
* that allows to read the latest value, and to write a new value. It's object identity
35+
* is stable, and does not change when writing a new value. That also means that it's
36+
* not reactive (you can't depend on it in useMemo or useEffect). For that, use the
37+
* second value from the returned tuple.
38+
* @param initialValue
39+
* @returns
40+
*/
41+
export function useSignal<T>(initialValue: T): [Signal<T>, T] {
42+
const [value, setValue] = useState(initialValue);
43+
const [signal] = useReducer(emptyReducer<Signal<T>>, { initialValue, setValue }, createSignal<T>);
44+
45+
return [signal, value] as const;
46+
}

0 commit comments

Comments
 (0)