NEW! We have now published an open source scichart-react library which neatly wraps up this entire tutorial and more. Handling component lifecycle, proper disposal of the SciChartSurface and with a nice API too :)
Head over to the following page to learn more:
React Charts with SciChart.js: Introducing the scichart-react library
Also see:
- SciChart-React on Github: official React component wrapper for SciChart.js
- SciChart-React on npm
The idea of this tutorial is to create a reusable React component which could be used as a core setup for instantiating a SciChart.js chart and properly disposing of memory once the chart is no longer needed.
Source code for this tutorial can be found at SciChart.Js.Examples Github Repository
Main criteria and points to consider:
- The component should be reusable for different chart configurations
- It should be possible to safely create several instances of the component
- It should be easy to add custom functionality to the component
- SciChart instantiation is an async function, thus it should be properly handled
- SciChart requires a root node element where it would reside to exist before the instantiation
- The chart should be properly disposed and memory deleted after the component is unmounted.
While creating the example we will try using tools provided in SciChart's MemoryUsageHelper to discover potential issues with memory leaking and bugs.
Worked Example - a Basic React Component
Let's start with showing an example of how to create a re-usable React Component to work with SciChart.js. We'll start off with a simple React component which instantiates a SciChartSurface in useEffect but fails to delete it on unmount.
Simple React Component |
Copy Code
|
---|---|
function SciChart() { const rootElementId = 'chart'; useEffect(() => { createChart(rootElementId); // Note, does not delete on unmount (todo later) }, []); return <div id={rootElementId} style={{width: 800, height: 600}} />; } |
where createChart
could be defined as below:
createChart |
Copy Code
|
---|---|
const createChart = async (divElementId: string ) => { const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId); const xAxis = new NumericAxis(wasmContext); const yAxis = new NumericAxis(wasmContext); sciChartSurface.xAxes.add(xAxis); sciChartSurface.yAxes.add(yAxis); return { sciChartSurface }; }; |
So by placing a createChart
into the useEffect
hook with an empty list of dependencies we ensure that:
- initialization will happen only once.
- and it will happen after the component render, so that it could create a chart on the rendered root element.
Reusing the component
Now let's make it reusable by allowing to provide arbitrary initialization function and styles. For this we could pass these via the component props.
Also, to allow using the result of the initialization we want to expose some interface to manipulate a chart. A common return result would contain a surface reference as createChart
has. To make it more generic we can defined the return type with ISciChartSurfaceBase which is common for 2D, 3D and Pie surfaces in SciChart.
Also, to allow placing multiple charts on the page we should provide a unique rootElementId
per component instance.
Updated useState |
Copy Code
|
---|---|
const [rootElementId] = useState(`chart-root-${generateGuid()}`); |
So the interface and usage of the props would look like this:
React Component |
Copy Code
|
---|---|
interface IChartComponentProps { initChart: (rootElementId: string) => Promise<{ sciChartSurface: ISciChartSurfaceBase }>; className?: string; style?: CSSProperties; } function SciChart(props: IChartComponentProps) { const [rootElementId] = useState(`chart-root-${generateGuid()}`); useEffect(() => { props.initChart(rootElementId); }, []); return <div id={rootElementId} className={props.className} style={props.style} />; } |
The usage example of such component:
usage |
Copy Code
|
---|---|
function App() { return ( <div className='App'> <SciChart initChart={createChart} style={{ width: 800, height: 600 }} /> </div> ); } |
Testing the component with SciChart Memory Debug tools
Now, before going to implementing the further requirements, let's try to test this component with SciCharts Memory Debugging tools.
We will setup an example in which we could force the unmounting of the SciChart
component to see if it has been properly cleaned up.
For this we will:
- enable the debug mode with
MemoryUsageHelper.isMemoryUsageDebugEnabled = true
, - hint the SciChart to automatically destroy the WebAssembly Context (if there are no surface instances using it) with
SciChartSurface.autoDisposeWasmContext = true
(to make a full cleanup for testing purposes).
MemoryUsageHelper.isMemoryUsageDebugEnabled = true
) is only supposed to be used development mode. It will not work in the production build.
Debugging memory leaks |
Copy Code
|
---|---|
// ... SciChartSurface.autoDisposeWasmContext = true; MemoryUsageHelper.isMemoryUsageDebugEnabled = true; function App() { const [drawChart, setDrawChart] = useState(true); const handleCheckbox: ChangeEventHandler<HTMLInputElement> = (e) => { setDrawChart(e.target.checked); }; const handleClick: MouseEventHandler<HTMLInputElement> = () => { const state = MemoryUsageHelper.objectRegistry.getState(); console.log('state', state); }; return ( <div className='App'> <header className='App-header'> <h1>SciChart.js with React</h1> <p>In this example we setup webpack, scichart, react and create a simple chart with one X and Y axis</p> </header> <input type='checkbox' checked={drawChart} onChange={handleCheckbox} /> Show Chart <br /> <input type='button' onClick={handleClick} value="Log Object Registry State"></input> {drawChart ? <SciChart initChart={createChart} style={{ width: 800, height: 600 }} /> : null} </div> ); } |
Now let's compare how the state of MemoryUsageHelper.objectRegistry will change after we unmount the component.
We will use this debugging steps for testingL
- Output the current state by pressing "Log Object Registry State" button. The output will be something like:
Console output Copy Codestate {undeletedObjectsIds: Array(61), uncollectedObjectsIds: Array(62)}
NOTE SciChart manages the lifecycle of most of them internally. But some of those, that were not attached to or were detached from a chart, are supposed to be managed explicitly.
- To unmount the component we could click the "Show Chart" checkbox. Immediately we will get a console warning
It suggest that we may have forgotten to clean up the created chart when the component was unmounted (which we did).
-
Now, additionally, we may force the garbage collection using browser tools if available. This may help to better understand if there are leaks of JS objects caused by unreleased references based on
uncollectedObjectsIds
. But more important is to focus on making sure that SciChart related entities were properly disposed and there should beundeletedObjectsIds
. -
Pressing the "Log Object Registry State" button again will show an output similar to the previous, confirming that the chart was not cleaned up properly.
Adding a cleanup callback
Let's try to fix an obvious issue and return a component destructor
in the useEffect
hook. Here we updated the component, but there is still an issue with it.
React component with cleanup (1) |
Copy Code
|
---|---|
function SciChart(props: IChartComponentProps) { const [sciChartSurface, setSciChartSurface] = useState<ISciChartSurfaceBase>(); const [rootElementId] = useState(`chart-root-${generateGuid()}`); useEffect(() => { (async () => { const res = await props.initChart(rootElementId); setSciChartSurface(res.sciChartSurface); })(); return () => sciChartSurface?.delete(); }, []); return <div id={rootElementId} className={props.className} style={props.style} />; } |
After going through the debug steps again we will see that nothing has really changed. The reason for this is a wrong reference to the surface used in the destructor () => sciChartSurface?.delete();
. As you may guess it will use the nullish value assigned in the initial component render.
So deal with the fact that chart initialization is an async operation we will consider handling the unresolved promise.
So our next iteration of the component will be:
React component with cleanup (2) |
Copy Code
|
---|---|
function SciChart(props: IChartComponentProps) { const sciChartSurfaceRef = useRef<ISciChartSurfaceBase>(); const [rootElementId] = useState(`chart-root-${generateGuid()}`); useEffect(() => { const chartInitializationPromise = props.initChart(rootElementId).then((initResult) => { sciChartSurfaceRef.current = initResult.sciChartSurface; return initResult.sciChartSurface; }); const performCleanup = () => { sciChartSurfaceRef.current.delete(); sciChartSurfaceRef.current = undefined; }; return () => { // check if chart is already initialized or wait init to finish before deleting it sciChartSurfaceRef.current ? performCleanup() : chartInitializationPromise.then(performCleanup); }; }, []); return <div id={rootElementId} className={props.className} style={props.style} />; } |
After running the debug steps again we should see an empty state returned to the output, which means the chart was properly garbage collected.
And with this we've achieved our goal of creating a reusable chart component. This component could be a boilerplate for further improvement and customization, so consider testing corner cases with the SciChart debugging tools, as well as tools provided by browsers.
Additional Tips
React:
- To allow adding custom functionality upon a chart component (e.g. binding UI controls for chart manipulations), consider exposing a reference to the surface via
useImperativeHandle
hook. - Consider using
Suspense
for awaiting the async data/component load
Debugging:
- MemoryUsageHelper exposes a number of methods for performing a cleanup. They may be useful for testing purposes but use them with caution.