Skip to content

TypeError: can't access property "from", k is null — race condition between create() and setOption() on mount #16

@dogmatic69

Description

@dogmatic69

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 → instance

setOption 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 setOption fires → works
  • Slow init (cold GPU context, complex chart, React concurrent batching, multiple charts on page): setOption fires 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 receives resolveOptions() — the first setOption is always redundant.
  • If options/theme change while create() is in flight, the effect will still run after chartInstance is set. Since the skip applies only once, the next invocation will correctly call setOption.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions