Skip to content

Commit 815d4b3

Browse files
authored
Merge pull request #3 from hunterg325/add-react-wrapper
Add React hook wrapper for ChartGPU
2 parents 04d44bf + 6ff2aaa commit 815d4b3

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export type {
1515
MouseOverParams,
1616
} from './types';
1717

18+
// useChartGPU hook (Story 6.19)
19+
export { useChartGPU } from './useChartGPU';
20+
export type { UseChartGPUResult } from './useChartGPU';
21+
1822
/**
1923
* @deprecated Use `ChartGPU` instead. `ChartGPUChart` is kept for backward compatibility.
2024
* Will be removed in a future major version.

src/useChartGPU.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { ChartGPU as ChartGPULib } from 'chartgpu';
3+
import type { ChartGPUOptions } from 'chartgpu';
4+
import type { ChartInstance } from './types';
5+
6+
/**
7+
* Result object returned by the useChartGPU hook.
8+
*/
9+
export interface UseChartGPUResult {
10+
/**
11+
* The ChartGPU instance once initialized, null before initialization.
12+
*/
13+
chart: ChartInstance | null;
14+
15+
/**
16+
* True when the chart has been successfully initialized and is ready to use.
17+
*/
18+
isReady: boolean;
19+
20+
/**
21+
* Error object if initialization failed or WebGPU is not supported.
22+
* Null when no error has occurred.
23+
*/
24+
error: Error | null;
25+
}
26+
27+
/**
28+
* Debounce utility for throttling frequent calls.
29+
*/
30+
function debounce<T extends (...args: any[]) => void>(
31+
fn: T,
32+
delayMs: number
33+
): (...args: Parameters<T>) => void {
34+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
35+
return (...args: Parameters<T>) => {
36+
if (timeoutId !== null) {
37+
clearTimeout(timeoutId);
38+
}
39+
timeoutId = setTimeout(() => {
40+
fn(...args);
41+
}, delayMs);
42+
};
43+
}
44+
45+
/**
46+
* React hook for managing a ChartGPU instance.
47+
*
48+
* Provides lifecycle management, automatic resize handling, and error handling
49+
* for ChartGPU charts in React applications.
50+
*
51+
* Features:
52+
* - Async initialization with StrictMode safety
53+
* - WebGPU support detection
54+
* - Automatic resize handling via ResizeObserver (debounced 100ms)
55+
* - Options updates via setOption
56+
* - Proper cleanup on unmount
57+
*
58+
* @param containerRef - React ref to the container element where the chart will be rendered
59+
* @param options - ChartGPU configuration options
60+
* @returns Object containing chart instance, ready state, and error state
61+
*
62+
* @example
63+
* ```tsx
64+
* const containerRef = useRef<HTMLDivElement>(null);
65+
* const { chart, isReady, error } = useChartGPU(containerRef, {
66+
* series: [{ type: 'line', data: [...] }],
67+
* xAxis: { type: 'linear' },
68+
* yAxis: { type: 'linear' }
69+
* });
70+
*
71+
* if (error) return <div>WebGPU not supported</div>;
72+
* if (!isReady) return <div>Loading...</div>;
73+
*
74+
* return <div ref={containerRef} style={{ width: '100%', height: '400px' }} />;
75+
* ```
76+
*/
77+
export function useChartGPU(
78+
containerRef: React.RefObject<HTMLElement>,
79+
options: ChartGPUOptions
80+
): UseChartGPUResult {
81+
const [chart, setChart] = useState<ChartInstance | null>(null);
82+
const [isReady, setIsReady] = useState(false);
83+
const [error, setError] = useState<Error | null>(null);
84+
85+
const mountedRef = useRef<boolean>(false);
86+
const resizeObserverRef = useRef<ResizeObserver | null>(null);
87+
88+
// Initialize chart on mount
89+
useEffect(() => {
90+
// WebGPU support check
91+
if (!('gpu' in navigator)) {
92+
setError(new Error('WebGPU not supported in this browser'));
93+
return;
94+
}
95+
96+
if (!containerRef.current) {
97+
return;
98+
}
99+
100+
mountedRef.current = true;
101+
let chartInstance: ChartInstance | null = null;
102+
103+
const initChart = async () => {
104+
try {
105+
if (!containerRef.current) return;
106+
107+
chartInstance = await ChartGPULib.create(
108+
containerRef.current,
109+
options
110+
);
111+
112+
// StrictMode safety: only update state if still mounted
113+
if (mountedRef.current) {
114+
setChart(chartInstance);
115+
setIsReady(true);
116+
setError(null);
117+
} else {
118+
// Component unmounted during async create - dispose immediately
119+
chartInstance.dispose();
120+
}
121+
} catch (err) {
122+
if (mountedRef.current) {
123+
// Normalize error to Error instance
124+
const normalizedError =
125+
err instanceof Error ? err : new Error(String(err));
126+
setError(normalizedError);
127+
setIsReady(false);
128+
}
129+
}
130+
};
131+
132+
initChart();
133+
134+
// Cleanup on unmount
135+
return () => {
136+
mountedRef.current = false;
137+
setIsReady(false);
138+
139+
if (chartInstance && !chartInstance.disposed) {
140+
chartInstance.dispose();
141+
}
142+
143+
if (resizeObserverRef.current) {
144+
resizeObserverRef.current.disconnect();
145+
resizeObserverRef.current = null;
146+
}
147+
};
148+
// Intentionally omitting containerRef.current from dependencies to avoid re-initialization
149+
// eslint-disable-next-line react-hooks/exhaustive-deps
150+
}, []);
151+
152+
// Update chart when options change
153+
useEffect(() => {
154+
if (!chart || chart.disposed) return;
155+
156+
chart.setOption(options);
157+
}, [chart, options]);
158+
159+
// Set up ResizeObserver for responsive sizing (debounced 100ms)
160+
useEffect(() => {
161+
const container = containerRef.current;
162+
if (!chart || chart.disposed || !container) return;
163+
164+
const debouncedResize = debounce(() => {
165+
if (chart && !chart.disposed) {
166+
chart.resize();
167+
}
168+
}, 100);
169+
170+
const observer = new ResizeObserver(() => {
171+
debouncedResize();
172+
});
173+
174+
observer.observe(container);
175+
resizeObserverRef.current = observer;
176+
177+
return () => {
178+
observer.disconnect();
179+
resizeObserverRef.current = null;
180+
};
181+
// Intentionally omitting containerRef.current from dependencies
182+
// eslint-disable-next-line react-hooks/exhaustive-deps
183+
}, [chart]);
184+
185+
return { chart, isReady, error };
186+
}

0 commit comments

Comments
 (0)