-
Notifications
You must be signed in to change notification settings - Fork 1
Description
When using <ChartGPU> in a React application, the component intermittently crashes with:
TypeError: can't access property "from", k is null
F createRenderCoordinator.ts:1361
G createRenderCoordinator.ts:2549
Ge createRenderCoordinator.ts:2561
setOption ChartGPU.ts:1731
The error is non-deterministic — the same component with identical data/options will fail on one render and succeed on the next (e.g., after the error boundary remounts).
All AI below:
Root cause
There is a race condition in the ChartGPU React component between the async create() initialization and the setOption useEffect.
Mount effect (deps: []) does:
const instance = await ChartGPU.create(container, options);
if (mounted) setState(instance); // y goes null → instancesetOption effect (deps: [instance, options, theme, resolveOptions]) does:
if (!instance || instance.disposed) return;
instance.setOption(resolveOptions());When setState(instance) fires after create() resolves, React schedules the setOption effect because its instance dependency changed (null → instance). This effect calls instance.setOption() immediately — but ChartGPU.create() already received the same options and may still be processing internal async work (computing axis ranges, allocating GPU buffers, etc.).
The setOption() call interrupts this incomplete initialization, hitting a null axis range object inside createRenderCoordinator.ts — hence k.from throws.
Why it’s intermittent
The race outcome depends on timing:
- Fast init (warm GPU context, simple chart): internal async work completes before
setOptionfires → works - Slow init (cold GPU context, complex chart, React concurrent batching, multiple charts on page):
setOptionfires before internal state is ready → crash - Works on retry: error boundary remounts; GPU resources are now warm → race resolves safely
Reproduction
Most reliably reproduced with:
- Multiple
<ChartGPU>instances on the same page (e.g., scatter charts) - First page load (cold GPU context)
- React
StrictMode(double-invokes effects, widening the race window)
Expected behavior
The component should not call setOption() redundantly on mount when create() already received the same options.
Affected version
chartgpu-react@0.1.3
Proposed fix
Skip the redundant setOption on mount. Since create() already receives the initial options, the setOption effect should only fire for subsequent option changes, not the initial null → instance transition.
The same fix applies to the useChartGPU hook (identical pattern).
Diff (against the unminified source)
// Inside the ChartGPU component
+const isInitialMount = useRef(true);
// Mount effect — creates the instance with initial options
useEffect(() => {
mounted.current = true;
const opts = resolveOptions();
const instance = await ChartGPU.create(containerRef.current, opts);
if (mounted.current) {
chartRef.current = instance;
setChartInstance(instance); // triggers setOption effect
onReady?.(instance);
}
return () => {
mounted.current = false;
instance.dispose();
setChartInstance(null);
};
}, []);
// setOption effect — updates options when they change
useEffect(() => {
if (!chartInstance || chartInstance.disposed) return;
+ // Skip the first invocation triggered by create() setting the instance.
+ // create() already received these options; calling setOption immediately
+ // races with internal async initialization.
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ return;
+ }
chartInstance.setOption(resolveOptions());
}, [chartInstance, options, theme, resolveOptions]);Why this is safe
create()already receivesresolveOptions()— the firstsetOptionis always redundant.- If
options/themechange whilecreate()is in flight, the effect will still run afterchartInstanceis set. Since the skip applies only once, the next invocation will correctly callsetOption. - The ref is scoped to the component instance, so remounts get a fresh
true.
Alternative / additional fix
If skipping mount entirely feels too coarse, an alternative is to make create() return a promise/signal that the instance is “ready” (all internal async work complete), and gate the setOption effect on that readiness. This would be a deeper change inside @chartgpu/chartgpu core but would be the most robust solution.